/**
 * @file megaapi_impl.cpp
 * @brief Private implementation of the intermediate layer for the MEGA C++ SDK.
 *
 * (c) 2013-2020 by Mega Limited, Auckland, New Zealand
 *
 * This file is part of the MEGA SDK - Client Access Engine.
 *
 * Applications using the MEGA API must present a valid application key
 * and comply with the the rules set forth in the Terms of Service.
 *
 * The MEGA SDK is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 *
 * @copyright Simplified (2-clause) BSD License.
 *
 * You should have received a copy of the license along with this
 * program.
 */

#define _LARGE_FILES

#define _GNU_SOURCE 1
#define _FILE_OFFSET_BITS 64

#define USE_VARARGS
#define PREFER_STDARG
#include "megaapi_impl.h"

#include "mega/canceller.h"
#include "mega/common/node_key_data.h"
#include "mega/common/utility.h"
#include "mega/file_service/file.h"
#include "mega/file_service/file_callbacks.h"
#include "mega/file_service/file_id.h"
#include "mega/file_service/file_info.h"
#include "mega/file_service/file_service_context.h"
#include "mega/file_service/file_service_options.h"
#include "mega/file_service/file_service_result.h"
#include "mega/file_service/file_stream_result.h"
#include "mega/file_service/utility.h"
#include "mega/logging.h"
#include "mega/mediafileattribute.h"
#include "mega/scoped_helpers.h"
#include "mega/tlv.h"
#include "mega/types.h"
#include "mega/user_attribute.h"
#include "mega/utils_optional.h"
#include "megaapi.h"

#ifdef ENABLE_ISOLATED_GFX
#include "mega/gfx/isolatedprocess.h"
#endif

#include <algorithm>
#include <cctype>
#include <charconv>
#include <cstdint>
#include <functional>
#include <iomanip>
#include <locale>
#include <numeric>
#include <thread>

#ifndef _WIN32
#ifndef _LARGEFILE64_SOURCE
    #define _LARGEFILE64_SOURCE
#endif
#include <signal.h>
#endif


#ifdef __APPLE__
    #include <xlocale.h>
    #include <strings.h>
#endif

#ifdef _WIN32
#include <shlwapi.h>
#endif

#ifdef USE_OPENSSL
#include <openssl/rand.h>
#endif

#include <zxcvbn-c/zxcvbn.h>

// FUSE
#include <mega/fuse/common/mount_event_type.h>
#include <mega/fuse/common/mount_event.h>
#include <mega/fuse/common/mount_info.h>

namespace {

using ::mega::IGfxProvider;
using ::mega::MegaGfxProcessor;
using ::mega::GfxProc;
using ::mega::GfxProviderExternal;

#define HTTP_verbose_timed \
    LOG_verbose_timed(std::chrono::milliseconds{120'000}, std::chrono::milliseconds{100})

std::unique_ptr<GfxProc> createGfxProc(MegaGfxProcessor* processor)
{
    // createInternalGfxProvider could return nullptr
    std::unique_ptr<IGfxProvider> provider = processor ?
                                             std::make_unique<GfxProviderExternal>(processor) :
                                             IGfxProvider::createInternalGfxProvider();

    return provider ? std::make_unique<GfxProc>(std::move(provider)) : nullptr;
}

}
namespace mega {

MegaNodePrivate::MegaNodePrivate(const char *name, int type, int64_t size, int64_t ctime, int64_t mtime, uint64_t nodehandle,
                                 const string *nodekey, const string *fileattrstring, const char *fingerprint, const char *originalFingerprint, MegaHandle owner, MegaHandle parentHandle,
                                 const char *privateauth, const char *publicauth, bool ispublic, bool isForeign, const char *chatauth, bool isNodeKeyDecrypted)
: MegaNode()
{
    this->name = MegaApi::strdup(name);
    this->fingerprint = MegaApi::strdup(fingerprint);
    this->originalfingerprint = MegaApi::strdup(originalFingerprint);
    this->customAttrs = NULL;
    this->duration = -1;
    this->width = -1;
    this->height = -1;
    this->shortformat = -1;
    this->videocodecid = -1;
    this->latitude = INVALID_COORDINATE;
    this->longitude = INVALID_COORDINATE;
    this->type = type;
    this->size = size;
    this->ctime = ctime;
    this->mtime = mtime;
    this->nodehandle = nodehandle;
    this->parenthandle = parentHandle;
    mIsNodeKeyDecrypted = isNodeKeyDecrypted;
    this->fileattrstring.assign(fileattrstring->data(), fileattrstring->size());
    this->nodekey.assign(nodekey->data(), nodekey->size());
    this->changed = 0;
    this->thumbnailAvailable = (Node::hasfileattribute(fileattrstring, GfxProc::THUMBNAIL) != 0);
    this->previewAvailable = (Node::hasfileattribute(fileattrstring, GfxProc::PREVIEW) != 0);
    this->isPublicNode = ispublic;
    this->outShares = false;
    this->inShare = false;
    this->plink = NULL;
    this->mNewLinkFormat = false;
    this->sharekey = NULL;
    this->foreign = isForeign;
    this->children = NULL;
    this->owner = owner;
    this->mFavourite = false;
    this->mLabel = LBL_UNKNOWN;

    if (privateauth)
    {
        this->privateAuth = privateauth;
    }

    if (publicauth)
    {
        this->publicAuth = publicauth;
    }

    this->chatAuth = chatauth ? MegaApi::strdup(chatauth) : NULL;
}

MegaNodePrivate::MegaNodePrivate(MegaNode *node)
: MegaNode()
{
    this->name = MegaApi::strdup(node->getName());
    this->fingerprint = MegaApi::strdup(node->getFingerprint());
    this->originalfingerprint = MegaApi::strdup(node->getOriginalFingerprint());
    this->customAttrs = NULL;

    MegaNodePrivate *np = dynamic_cast<MegaNodePrivate *>(node);
    if (!np)
    {
        LOG_err << "Critical error: Unexpected MegaNode extension received";
        assert(false);
        return;
    }

    // Optimization to avoid decode media info when getter is called
    this->duration = np->duration;
    this->width = np->width;
    this->height = np->height;
    this->shortformat = np->shortformat;
    this->videocodecid = np->videocodecid;

    this->mFavourite = node->isFavourite();
    this->mLabel = static_cast<nodelabel_t>(node->getLabel());
    this->mDeviceId = node->getDeviceId();
    this->mS4 = node->getS4();
    this->mMarkedSensitive = node->isMarkedSensitive();
    if (np->mOfficialAttrs) this->mOfficialAttrs = std::make_unique<attr_map>(*np->mOfficialAttrs);
    this->latitude = node->getLatitude();
    this->longitude = node->getLongitude();
    this->restorehandle = node->getRestoreHandle();
    this->type = node->getType();
    this->size = node->getSize();
    this->ctime = node->getCreationTime();
    this->mtime = node->getModificationTime();
    this->nodehandle = node->getHandle();
    this->parenthandle = node->getParentHandle();
    mIsNodeKeyDecrypted = node->isNodeKeyDecrypted();
    char* fileAttributeString = node->getFileAttrString();
    if (fileAttributeString)
    {
        this->fileattrstring = std::string(fileAttributeString);
        delete [] fileAttributeString;
    }

    assert(node->getNodeKey() && "node key cannot be null");
    this->nodekey = *node->getNodeKey();
    this->changed = node->getChanges();
    this->thumbnailAvailable = node->hasThumbnail();
    this->previewAvailable = node->hasPreview();
    this->isPublicNode = node->isPublic();
    this->privateAuth = *np->getPrivateAuth();
    this->publicAuth = *np->getPublicAuth();
    this->chatAuth = np->getChatAuth() ? MegaApi::strdup(np->getChatAuth()) : NULL;
    this->outShares = node->isOutShare();
    this->inShare = node->isInShare();
    this->foreign = node->isForeign();
    this->sharekey = NULL;
    this->children = NULL;
    this->owner = node->getOwner();

    if (node->isExported())
    {
        this->plink = new PublicLink(node->getPublicHandle(), node->getPublicLinkCreationTime(),
                                     node->getExpirationTime(), node->isTakenDown(), node->getWritableLinkAuthKey());

        if (type == FOLDERNODE)
        {
            MegaNodePrivate *n = dynamic_cast<MegaNodePrivate *>(node);
            if (n)
            {
                string *sk = n->getSharekey();
                if (sk)
                {
                    this->sharekey = new string(*sk);
                }
            }
        }
    }
    else
    {
        this->plink = NULL;
    }
    this->mNewLinkFormat = np->isNewLinkFormat();

    if (node->hasCustomAttrs())
    {
        this->customAttrs = new attr_map();
        MegaStringList *names = node->getCustomAttrNames();
        for (int i = 0; i < names->size(); i++)
        {
            (*customAttrs)[AttrMap::string2nameid(names->get(i))] = node->getCustomAttr(names->get(i));
        }
        delete names;
    }
}

MegaNodePrivate::MegaNodePrivate(Node *node)
: MegaNode()
{
    this->name = MegaApi::strdup(node->displayname());
    this->fingerprint = NULL;
    this->originalfingerprint = NULL;
    this->children = NULL;

    if (node->isvalid)
    {
        string tempFingerprint;
        node->serializefingerprint(&tempFingerprint);
        string result = MegaNodePrivate::addAppPrefixToFingerprint(tempFingerprint, node->size);
        fingerprint = MegaApi::strdup(result.c_str());
    }

    this->duration = -1;
    this->width = -1;
    this->height = -1;
    this->shortformat = -1;
    this->videocodecid = -1;
    this->latitude = INVALID_COORDINATE;
    this->longitude = INVALID_COORDINATE;
    this->customAttrs = NULL;
    this->restorehandle = UNDEF;
    this->mFavourite = false;
    this->mLabel = LBL_UNKNOWN;

    char buf[10];
    for (attr_map::iterator it = node->attrs.map.begin(); it != node->attrs.map.end(); it++)
    {
        int attrlen = node->attrs.nameid2string(it->first, buf);
        buf[attrlen] = '\0';
        if (buf[0] == '_')
        {
           if (!customAttrs)
           {
               customAttrs = new attr_map();
           }

           nameid id = AttrMap::string2nameid(&buf[1]);
           (*customAttrs)[id] = it->second;
        }
        else
        {
            if (it->first == AttrMap::string2nameid("d"))
            {
               if (node->type == FILENODE)
               {
                   duration = int(Base64::atoi(&it->second));
               }
            }
            else if (it->first == AttrMap::string2nameid("l") || it->first == AttrMap::string2nameid("gp"))
            {
                if (node->type == FILENODE)
                {
                    string coords = it->second;
                    if ((it->first == AttrMap::string2nameid("l") && coords.size() != 8) ||
                        (it->first == AttrMap::string2nameid("gp") && coords.size() != Base64Str<16>::STRLEN))
                    {
                       LOG_warn << "Malformed GPS coordinates attribute";
                    }
                    else
                    {
                        bool ok = true;
                        if (it->first == AttrMap::string2nameid("gp"))
                        {
                            if (node->client && node->client->unshareablekey.size() == Base64Str<SymmCipher::KEYLENGTH>::STRLEN && coords.size() == Base64Str<16>::STRLEN)
                            {
                                SymmCipher c;
                                byte data[SymmCipher::BLOCKSIZE] = { 0 };
                                Base64::atob(coords.data(), data, Base64Str<SymmCipher::BLOCKSIZE>::STRLEN);

                                node->client->setkey(&c, node->client->unshareablekey.data());
                                c.ctr_crypt(data, SymmCipher::BLOCKSIZE, 0, 0, NULL, false);
                                ok = Utils::startswith(reinterpret_cast<const char*>(data),
                                                       "unshare/");
                                if (ok)
                                {
                                    coords = string((char*)data + 8, 8);
                                }
                            }
                            else
                            {
                                ok = false;
                            }
                        }

                        if (ok)
                        {
                            byte bufCoords[3];
                            int number = 0;
                            if (Base64::atob((const char*)coords.substr(0, 4).data(),
                                             bufCoords,
                                             sizeof(bufCoords)) == sizeof(bufCoords))
                            {
                                number =
                                    (bufCoords[2] << 16) | (bufCoords[1] << 8) | (bufCoords[0]);
                                latitude = -90 + 180 * (double)number / 0xFFFFFF;
                            }

                            if (Base64::atob((const char*)coords.substr(4, 4).data(),
                                             bufCoords,
                                             sizeof(bufCoords)) == sizeof(bufCoords))
                            {
                                number =
                                    (bufCoords[2] << 16) | (bufCoords[1] << 8) | (bufCoords[0]);
                                longitude = -180 + 360 * (double)number / 0x01000000;
                            }
                        }
                    }

                    if (longitude < -180 || longitude > 180)
                    {
                        longitude = INVALID_COORDINATE;
                    }
                    if (latitude < -90 || latitude > 90)
                    {
                        latitude = INVALID_COORDINATE;
                    }
                    if (longitude == INVALID_COORDINATE || latitude == INVALID_COORDINATE)
                    {
                        longitude = INVALID_COORDINATE;
                        latitude = INVALID_COORDINATE;
                    }
               }
            }
            else if (it->first == AttrMap::string2nameid("rr"))
            {
                handle rr = 0;
                if (Base64::atob(it->second.c_str(), (byte *)&rr, sizeof(rr)) == MegaClient::NODEHANDLE)
                {
                    restorehandle = rr;
                }
            }
            else if (it->first == AttrMap::string2nameid("c") && !fingerprint)
            {
                fingerprint = MegaApi::strdup(it->second.c_str());
            }
            else if (it->first == AttrMap::string2nameid("c0"))
            {
                originalfingerprint = MegaApi::strdup(it->second.c_str());
            }
            else if (it->first == AttrMap::string2nameid("fav"))
            {
                try
                {
                    int fav = it->second.empty() ? 0 : std::stoi(it->second);
                    if (fav != 1 && it->second != "0")
                    {
                        LOG_err << "Invalid value for node attr fav: " << fav;
                    }
                    else
                    {
                        mFavourite = fav != 0;
                    }
                }
                catch (std::exception& ex)
                {
                    LOG_err << "Conversion failure for node attr fav: " << ex.what();
                }
            }
            else if (it->first == AttrMap::string2nameid("sen"))
            {
                try
                {
                    int sen = it->second.empty() ? 0 : std::stoi(it->second);
                    if (sen != 1 && it->second != "0")
                    {
                        LOG_err << "Invalid value for node attr sen: " << sen;
                    }
                    else
                    {
                        mMarkedSensitive = sen != 0;
                    }
                }
                catch (std::exception& ex)
                {
                    LOG_err << "Conversion failure for node attr sen: " << ex.what();
                }
            }
            else if (it->first == AttrMap::string2nameid("lbl"))
            {
                try
                {
                    int lbl = it->second.empty() ? LBL_UNKNOWN : std::stoi(it->second);
                    if ((lbl < LBL_RED || lbl > LBL_GREY)  && it->second != "0")
                    {
                        LOG_err << "Invalid value for node attr lbl: " << lbl;
                    }
                    else
                    {
                        mLabel = static_cast<nodelabel_t>(lbl);
                    }
                }
                catch (std::exception& ex)
                {
                    LOG_err << "Conversion failure for node attr lbl: " << ex.what();
                }
            }
            else if (it->first == AttrMap::string2nameid("dev-id") ||
                     it->first == AttrMap::string2nameid("drv-id"))
            {
                mDeviceId = it->second;
            }
            else if (it->first == AttrMap::string2nameid("s4"))
            {
                mS4 = it->second;
            }
            else if (it->first == AttrMap::string2nameid(MegaClient::NODE_ATTR_PASSWORD_MANAGER) ||
                     it->first == AttrMap::string2nameid(MegaClient::NODE_ATTRIBUTE_DESCRIPTION) ||
                     it->first == AttrMap::string2nameid(MegaClient::NODE_ATTRIBUTE_TAGS))
            {
                if (!mOfficialAttrs) mOfficialAttrs = std::make_unique<attr_map>();

                (*mOfficialAttrs)[it->first] = it->second;
            }
        }
    }

    this->type = node->type;
    this->size = node->size;
    this->ctime = node->ctime;
    this->mtime = node->mtime;
    this->nodehandle = node->nodehandle;
    this->parenthandle = node->parent ? node->parent->nodehandle : INVALID_HANDLE;
    this->owner = node->owner;

    mIsNodeKeyDecrypted = node->attrstring == nullptr;  // it's reset after node's key decryption successfull
    this->fileattrstring = node->fileattrstring;
    this->nodekey = node->nodekeyUnchecked();

    this->changed = 0;
    if(node->changed.attrs)
    {
        this->changed |= MegaNode::CHANGE_TYPE_ATTRIBUTES;
    }
    if(node->changed.ctime)
    {
        this->changed |= MegaNode::CHANGE_TYPE_TIMESTAMP;
    }
    if(node->changed.fileattrstring)
    {
        this->changed |= MegaNode::CHANGE_TYPE_FILE_ATTRIBUTES;
    }
    if(node->changed.inshare)
    {
        this->changed |= MegaNode::CHANGE_TYPE_INSHARE;
    }
    if(node->changed.outshares)
    {
        this->changed |= MegaNode::CHANGE_TYPE_OUTSHARE;
    }
    if(node->changed.pendingshares)
    {
        this->changed |= MegaNode::CHANGE_TYPE_PENDINGSHARE;
    }
    if(node->changed.owner)
    {
        this->changed |= MegaNode::CHANGE_TYPE_OWNER;
    }
    if(node->changed.parent)
    {
        this->changed |= MegaNode::CHANGE_TYPE_PARENT;
    }
    if(node->changed.removed)
    {
        this->changed |= MegaNode::CHANGE_TYPE_REMOVED;
    }
    if(node->changed.publiclink)
    {
        this->changed |= MegaNode::CHANGE_TYPE_PUBLIC_LINK;
    }
    if(node->changed.newnode)
    {
        this->changed |= MegaNode::CHANGE_TYPE_NEW;
    }
    if (node->changed.name)
    {
        this->changed |= MegaNode::CHANGE_TYPE_NAME;
    }
    if (node->changed.favourite)
    {
        this->changed |= MegaNode::CHANGE_TYPE_FAVOURITE;
    }
    if (node->changed.counter)
    {
        this->changed |= MegaNode::CHANGE_TYPE_COUNTER;
    }
    if (node->changed.sensitive)
    {
        this->changed |= MegaNode::CHANGE_TYPE_SENSITIVE;
    }
    if (node->changed.pwd)
    {
        this->changed |= MegaNode::CHANGE_TYPE_PWD;
    }
    if (node->changed.description)
    {
        this->changed |= MegaNode::CHANGE_TYPE_DESCRIPTION;
    }
    if (node->changed.tags)
    {
        this->changed |= MegaNode::CHANGE_TYPE_TAGS;
    }

    this->thumbnailAvailable = (node->hasfileattribute(0) != 0);
    this->previewAvailable = (node->hasfileattribute(1) != 0);
    this->isPublicNode = false;
    this->foreign = false;

    // if there's only one share and it has no user --> public link
    this->outShares = (node->outshares) ? (node->outshares->size() > 1 || node->outshares->begin()->second->user) : false;
    this->inShare = node->inshare != nullptr;
    this->plink = node->plink ? new PublicLink(*node->plink) : NULL;
    this->mNewLinkFormat = node->client->mNewLinkFormat;
    if (plink && type == FOLDERNODE && node->sharekey)
    {
        char key[FOLDERNODEKEYLENGTH*4/3+3];
        Base64::btoa(node->sharekey->key, FOLDERNODEKEYLENGTH, key);
        this->sharekey = new string(key);
    }
    else
    {
        this->sharekey = NULL;
    }
}

string* MegaNodePrivate::getSharekey()
{
    return sharekey;
}

MegaNode *MegaNodePrivate::copy()
{
    return new MegaNodePrivate(this);
}

char *MegaNodePrivate::serialize()
{
    string d;
    if (!serialize(&d))
    {
        return NULL;
    }

    char *ret = new char[d.size()*4/3+3];
    Base64::btoa((byte*)d.data(), d.size(), ret);

    return ret;
}

bool MegaNodePrivate::serialize(string *d) const
{
    CacheableWriter w(*d);
    w.serializecstr(name, true);
    w.serializecstr(fingerprint, true);
    w.serializei64(size);
    w.serializei64(ctime);
    w.serializei64(mtime);
    w.serializehandle(nodehandle);
    w.serializehandle(parenthandle);
    w.serializestring("");  // used to be attrstring
    w.serializestring(nodekey);
    w.serializestring(privateAuth);
    w.serializestring(publicAuth);
    w.serializebool(isPublicNode);
    w.serializebool(foreign);

    bool hasChatAuth = chatAuth && chatAuth[0];
    bool hasOwner = true;

    bool hasOriginalFingerprint = originalfingerprint && originalfingerprint[0];

    w.serializeexpansionflags(hasChatAuth, hasOwner, hasOriginalFingerprint, mIsNodeKeyDecrypted);

    if (hasChatAuth)
    {
        w.serializecstr(chatAuth, false);
    }
    if (hasOwner)
    {
        w.serializehandle(owner);
    }
    if (hasOriginalFingerprint)
    {
        w.serializecstr(originalfingerprint, false);
    }
    // 4th boolean in expansion flags will be set to the boolean value directly

    return true;
}

MegaNodePrivate *MegaNodePrivate::unserialize(string *d)
{
    CacheableReader r(*d);
    string name, fingerprint, originalfingerprint, attrstring, nodekey, privauth, pubauth, chatauth;
    int64_t size, ctime, mtime;
    MegaHandle nodehandle, parenthandle, owner = INVALID_HANDLE;
    bool isPublic, isForeign, isNodeKeyDecrypted;
    unsigned char expansions[8];
    string fileattrstring; // fileattrstring is not serialized
    if (!r.unserializecstr(name, true) || !r.unserializecstr(fingerprint, true) ||
        !r.unserializei64(size) || !r.unserializei64(ctime) || !r.unserializei64(mtime) ||
        !r.unserializehandle(nodehandle) || !r.unserializehandle(parenthandle) ||
        !r.unserializestring(attrstring) || !r.unserializestring(nodekey) ||
        !r.unserializestring(privauth) || !r.unserializestring(pubauth) ||
        !r.unserializebool(isPublic) || !r.unserializebool(isForeign) ||
        !r.unserializeexpansionflags(expansions, 4) ||
        (expansions[0] && !r.unserializecstr(chatauth, false)) ||
        (expansions[1] && !r.unserializehandle(owner)) ||
        (expansions[2] && !r.unserializecstr(originalfingerprint, false)))
    {
        LOG_err << "MegaNode unserialization failed at field " << r.fieldnum;
        return NULL;
    }
    isNodeKeyDecrypted = expansions[3] > 0; // the expansion flag is used to represent its value

    r.eraseused(*d);

    return new MegaNodePrivate(name.c_str(),
                               FILENODE,
                               size,
                               ctime,
                               mtime,
                               nodehandle,
                               &nodekey,
                               &fileattrstring,
                               fingerprint.empty() ? NULL : fingerprint.c_str(),
                               originalfingerprint.empty() ? NULL : originalfingerprint.c_str(),
                               owner,
                               parenthandle,
                               privauth.c_str(),
                               pubauth.c_str(),
                               isPublic,
                               isForeign,
                               chatauth.empty() ? NULL : chatauth.c_str(),
                               isNodeKeyDecrypted);
}

char *MegaNodePrivate::getBase64Handle()
{
    char *base64Handle = new char[12];
    Base64::btoa((byte*)&(nodehandle),MegaClient::NODEHANDLE,base64Handle);
    return base64Handle;
}

int MegaNodePrivate::getType() const
{
    return type;
}

const char* MegaNodePrivate::getName()
{
    if(type <= FOLDERNODE)
    {
        return name;
    }

    switch(type)
    {
        case ROOTNODE:
            return "Cloud Drive";
        case VAULTNODE:
            return "Vault";
        case RUBBISHNODE:
            return "Rubbish Bin";
        default:
            return name;
    }
}

const char *MegaNodePrivate::getFingerprint()
{
    return fingerprint;
}

const char *MegaNodePrivate::getOriginalFingerprint()
{
    return originalfingerprint;
}

bool MegaNodePrivate::hasCustomAttrs()
{
    return customAttrs != NULL;
}

MegaStringList *MegaNodePrivate::getCustomAttrNames()
{
    if (!customAttrs)
    {
        return new MegaStringList();
    }

    string_vector names;
    for (attr_map::iterator it = customAttrs->begin(); it != customAttrs->end(); it++)
    {
        names.push_back(AttrMap::nameid2string(it->first));
    }
    return new MegaStringListPrivate(std::move(names));
}

const char* MegaNodePrivate::getAttrFrom(const char *attrName, const attr_map* m) const
{
    if (!m)
    {
        return NULL;
    }

    nameid n = AttrMap::string2nameid(attrName);
    if (!n)
    {
        return NULL;
    }

    auto it = m->find(n);
    if (it == m->end())
    {
        return NULL;
    }

    return it->second.c_str();
}

const char *MegaNodePrivate::getCustomAttr(const char *attrName)
{
    return getAttrFrom(attrName, customAttrs);
}

const char *MegaNodePrivate::getOfficialAttr(const char *attrName) const
{
    return getAttrFrom(attrName, mOfficialAttrs.get());
}

template<typename PropertySelector>
auto MegaNodePrivate::getMediaProperty(PropertySelector selector)
    -> std::enable_if_t<MediaProperties::IsPropertySelectorV<PropertySelector>,
                        std::optional<MediaProperties::PropertyTypeT<PropertySelector>>>
{
    // Node isn't a file.
    if (type != MegaNode::TYPE_FILE)
        return std::nullopt;

    // Node hasn't been decrypted.
    if (nodekey.size() != FILENODEKEYLENGTH)
        return std::nullopt;

    // Node doesnt have any file attributes.
    if (fileattrstring.empty())
        return std::nullopt;

    // Convenience.
    auto fileKey = reinterpret_cast<std::uint32_t*>(nodekey.data() + FILENODEKEYLENGTH / 2);

    // Try and retrieve the specified property.
    return MediaProperties::getMediaProperty(fileattrstring, fileKey, selector);
}

int MegaNodePrivate::getDuration()
{
    if (auto playtime = getMediaProperty(&MediaProperties::playtime); playtime && *playtime)
        return static_cast<int>(*playtime);

    return duration;
}

bool MegaNodePrivate::isFavourite()
{
    return mFavourite;
}

bool MegaNodePrivate::isMarkedSensitive()
{
    return mMarkedSensitive;
}

int MegaNodePrivate::getLabel()
{
    return mLabel;
}

int MegaNodePrivate::getWidth()
{
    if (width >= 0)
        return width;

    if (auto w = getMediaProperty(&MediaProperties::width); w && *w)
        width = static_cast<int>(*w);

    return width;
}

int MegaNodePrivate::getHeight()
{
    if (height >= 0)
        return height;

    if (auto h = getMediaProperty(&MediaProperties::height); h && *h)
        height = static_cast<int>(*h);

    return height;
}

int MegaNodePrivate::getShortformat()
{
    if (shortformat >= 0)
        return shortformat;

    if (auto sf = getMediaProperty(&MediaProperties::shortformat); sf)
        shortformat = *sf;

    return shortformat;
}

int MegaNodePrivate::getVideocodecid()
{
    if (videocodecid >= 0)
        return videocodecid;

    if (auto vc = getMediaProperty(&MediaProperties::videocodecid); vc && *vc)
        videocodecid = static_cast<int>(*vc);

    return videocodecid;
}

double MegaNodePrivate::getLatitude()
{
    return latitude;
}

double MegaNodePrivate::getLongitude()
{
    return longitude;
}

const char* MegaNodePrivate::getDescription()
{
    return getOfficialAttr(MegaClient::NODE_ATTRIBUTE_DESCRIPTION);
}

MegaStringList* MegaNodePrivate::getTags()
{
    if (auto delimitedTags = getOfficialAttr(MegaClient::NODE_ATTRIBUTE_TAGS))
        return new MegaStringListPrivate(MegaClient::getNodeTags(delimitedTags));

    return new MegaStringListPrivate();
}

int64_t MegaNodePrivate::getSize()
{
    return size;
}

int64_t MegaNodePrivate::getCreationTime()
{
    return ctime;
}

int64_t MegaNodePrivate::getModificationTime()
{
    return mtime;
}

MegaHandle MegaNodePrivate::getRestoreHandle()
{
    return restorehandle;
}

MegaHandle MegaNodePrivate::getParentHandle()
{
    return parenthandle;
}

uint64_t MegaNodePrivate::getHandle() const
{
    return nodehandle;
}

string *MegaNodePrivate::getNodeKey()
{
    return &nodekey;
}

bool MegaNodePrivate::isNodeKeyDecrypted()
{
    return mIsNodeKeyDecrypted;
}

char *MegaNodePrivate::getBase64Key()
{
    char *key = NULL;

    // the key
    if (type == FILENODE && nodekey.size() >= FILENODEKEYLENGTH)
    {
        key = new char[FILENODEKEYLENGTH * 4 / 3 + 3];
        Base64::btoa((const byte*)nodekey.data(), FILENODEKEYLENGTH, key);
    }
    else if (type == FOLDERNODE && sharekey)
    {
        key = MegaApi::strdup(sharekey->c_str());
    }
    else
    {
        key = new char[1];
        key[0] = 0;
    }

    return key;
}

char *MegaNodePrivate::getFileAttrString()
{
    char* fileAttributes = NULL;

    if (fileattrstring.size() > 0)
    {
        fileAttributes = MegaApi::strdup(fileattrstring.c_str());
    }

    return fileAttributes;
}

int64_t MegaNodePrivate::getExpirationTime()
{
    return plink ? plink->ets : -1;
}

MegaHandle MegaNodePrivate::getPublicHandle()
{
    return plink ? (MegaHandle) plink->ph : INVALID_HANDLE;
}

MegaNode* MegaNodePrivate::getPublicNode()
{
    if (!plink || plink->isExpired())
    {
        return NULL;
    }

    char *skey = getBase64Key();
    string key(skey);

    MegaNode *node = new MegaNodePrivate(
                name, type, size, ctime, mtime,
                plink->ph, &key, &fileattrstring, fingerprint, originalfingerprint,
                INVALID_HANDLE);

    delete [] skey;

    return node;
}

char *MegaNodePrivate::getPublicLink(bool includeKey)
{
    if (!plink)
    {
        return NULL;
    }

    char *base64k = getBase64Key();
    TypeOfLink lType = MegaClient::validTypeForPublicURL(static_cast<nodetype_t>(type));
    string strlink = MegaClient::publicLinkURL(mNewLinkFormat, lType, plink->ph, (includeKey ? base64k : nullptr));
    delete [] base64k;

    return MegaApi::strdup(strlink.c_str());
}

int64_t MegaNodePrivate::getPublicLinkCreationTime()
{
    return plink ? plink->cts : -1;
}

const char *MegaNodePrivate::getWritableLinkAuthKey()
{
    return (plink && !plink->mAuthKey.empty()) ? plink->mAuthKey.c_str() : nullptr;
}

bool MegaNodePrivate::isNewLinkFormat()
{
    return mNewLinkFormat;
}

bool MegaNodePrivate::isFile()
{
    return type == TYPE_FILE;
}

bool MegaNodePrivate::isFolder()
{
    return (type != TYPE_FILE) && (type != TYPE_UNKNOWN);
}

bool MegaNodePrivate::isRemoved()
{
    return hasChanged(MegaNode::CHANGE_TYPE_REMOVED);
}

bool MegaNodePrivate::hasChanged(uint64_t changeType)
{
    return (changed & changeType);
}

uint64_t MegaNodePrivate::getChanges()
{
    return changed;
}

MegaHandle MegaNodePrivate::getOwner() const
{
    return owner;
}

const char* MegaNodePrivate::getDeviceId() const
{
    return mDeviceId.c_str();
}

const char* MegaNodePrivate::getS4() const
{
    return mS4.c_str();
}

string MegaNodePrivate::addAppPrefixToFingerprint(const string& fp, const m_off_t nodeSize)
{
    if (fp.empty())
    {
        LOG_warn << "Requesting app prefix addition to an empty fingerprint";
        return string{};
    }

    FileFingerprint ffp;
    if (!ffp.unserializefingerprint(&fp))
    {
        LOG_err << "Internal error: fingerprint validation failed in app prefix addition. Unserialization check failed";
        return string{};
    }

    byte bsize[sizeof(nodeSize) + 1];
    int l = Serialize64::serialize(bsize, static_cast<uint64_t>(nodeSize));
    unique_ptr<char[]> buf(new char[static_cast<size_t>(l * 4 / 3 + 4)]);
    char ssize = static_cast<char>('A' + Base64::btoa(bsize, static_cast<size_t>(l), buf.get()));

    string result(1, ssize);
    result.append(buf.get());
    result.append(fp);

    return result;
}

string MegaNodePrivate::removeAppPrefixFromFingerprint(const char* appFpParam, m_off_t* nodeSize)
{
    const std::string appFp = appFpParam ? appFpParam : "";
    if (appFp.empty())
    {
        LOG_warn << "Requesting app prefix removal from an empty fingerprint";
        return string{};
    }

    const size_t sizelen = static_cast<size_t>(appFp[0] - 'A');
    if (sizelen > (sizeof(m_off_t) * 4/3 + 4) || appFp.size() <= (sizelen + 1))
    {
        LOG_err << "Internal error: fingerprint validation failed. Fingerprint with sizelen: " << sizelen
                << " and fplen: " << appFp.size();
        return string{};
    }

    if (nodeSize)
    {
        m_off_t nSize = 0;
        int len = sizeof(nSize);
        auto buf = std::make_unique<byte[]>(static_cast<size_t>(len));
        Base64::atob(appFp.c_str() + 1, buf.get(), len);
        int l = Serialize64::unserialize(buf.get(), len, (uint64_t*)&nSize);
        if (l <= 0)
        {
            LOG_err << "Internal error: node size extraction from fingerprint failed";
            return string{};
        }
        *nodeSize = nSize;
    }

    FileFingerprint ffp;
    string result = appFp.substr(sizelen + 1);
    if (!ffp.unserializefingerprint(&result))
    {
        LOG_err << "Internal error: fingerprint unserialization failed in app prefix removal";
        return string{};
    }

    return result;
}

MegaBackgroundMediaUploadPrivate::MegaBackgroundMediaUploadPrivate(MegaApi* capi)
    : api(MegaApiImpl::ImplOf(capi))
{
    // generate fresh random encryption key/CTR IV for this file
    api->client->rng.genblock(filekey, sizeof filekey);
}

MegaBackgroundMediaUploadPrivate::MegaBackgroundMediaUploadPrivate(const string& serialised, MegaApi* capi)
    : api(MegaApiImpl::ImplOf(capi))
{
    CacheableReader r(serialised);
    string mediapropertiesstr;
    unsigned char expansions[8];
    string fileattrstring; // fileattrstring is not serialized
    if (!r.unserializebinary(filekey, sizeof(filekey)) ||
        !r.unserializechunkmacs(chunkmacs) ||
        !r.unserializestring(mediapropertiesstr) ||
        !r.unserializestring(url) ||
        !r.unserializedouble(latitude) ||
        !r.unserializedouble(longitude) ||
        !r.unserializebool(unshareableGPS) ||
        !r.unserializehandle(thumbnailFA) ||
        !r.unserializehandle(previewFA) ||
        !r.unserializeexpansionflags(expansions, 0))
    {
        LOG_err << "MegaBackgroundMediaUploadPrivate unserialization failed at field " << r.fieldnum;
    }
    else
    {
        mediaproperties = MediaProperties(mediapropertiesstr);
    }
}

bool MegaBackgroundMediaUploadPrivate::serialize(string* s)
{
    CacheableWriter w(*s);
    w.serializebinary(filekey, sizeof(filekey));
    w.serializechunkmacs(chunkmacs);
    w.serializestring(mediaproperties.serialize());
    w.serializestring(url);
    w.serializedouble(latitude);
    w.serializedouble(longitude);
    w.serializebool(unshareableGPS);
    w.serializehandle(thumbnailFA);
    w.serializehandle(previewFA);
    w.serializeexpansionflags();  // if/when we add more in future, set the first one true to signal the new set are present.
    return s != nullptr;
}

char *MegaBackgroundMediaUploadPrivate::serialize()
{
    string d;
    return serialize(&d) ? MegaApi::binaryToBase64(d.data(), d.size()) : NULL;
}

void MegaBackgroundMediaUploadPrivate::setThumbnail(MegaHandle h)
{
    thumbnailFA = h;
}

void MegaBackgroundMediaUploadPrivate::setPreview(MegaHandle h)
{
    previewFA = h;
}

void MegaBackgroundMediaUploadPrivate::setCoordinates(double lat, double lon, bool unsh)
{
    latitude = lat;
    longitude = lon;
    unshareableGPS = unsh;
}

SymmCipher* MegaBackgroundMediaUploadPrivate::nodecipher(MegaClient* client)
{
    return client->getRecycledTemporaryNodeCipher(filekey);
}

MegaBackgroundMediaUploadPrivate::~MegaBackgroundMediaUploadPrivate()
{
}

bool MegaBackgroundMediaUploadPrivate::analyseMediaInfo([[maybe_unused]] const char* inputFilepath)
{
#ifdef USE_MEDIAINFO
    std::lock_guard<std::recursive_timed_mutex> g(api->sdkMutex);
    if (!api->client->mediaFileInfo.mediaCodecsReceived)
    {
        // the client app should already have requested these but just in case:
        api->client->mediaFileInfo.requestCodecMappingsOneTime(api->client, LocalPath());
        return false;
    }

    auto localfilename = LocalPath::fromAbsolutePath(inputFilepath);

    string ext;
    if (api->fsAccess->getextension(localfilename, ext) && MediaProperties::isMediaFilenameExt(ext))
    {
        mediaproperties.extractMediaPropertyFileAttributes(localfilename, api->fsAccess.get());

        // cause the codec IDs to be looked up before serialization. Codec names are not serialized, just the codec IDs
        uint32_t dummykey[4];
        mediaproperties.convertMediaPropertyFileAttributes(dummykey, api->client->mediaFileInfo);
    }
#endif
    return true;
}

char *MegaBackgroundMediaUploadPrivate::encryptFile(const char* inputFilepath, int64_t startPos, int64_t* length, const char* outputFilepath, bool adjustsizeonly)
{
    if (startPos != ChunkedHash::chunkfloor(startPos))
    {
        LOG_err << "non-chunk start position supplied";
        return nullptr;
    }

    std::unique_ptr<FileAccess> fain(api->fsAccess->newfileaccess());
    auto localfilename = LocalPath::fromAbsolutePath(inputFilepath);

    if (fain->fopen(localfilename, OPEN_RDONLY, FSLogging::logOnError) || fain->type != FILENODE)
    {
        if (*length == -1)
        {
            *length = fain->size - startPos;
        }
        if (startPos < 0 || startPos > fain->size)
        {
            LOG_err << "invalid startPos supplied";
            return nullptr;
        }
        else if (*length < 0 || startPos + *length > fain->size)
        {
            LOG_err << "invalid enryption length supplied";
            return nullptr;
        }
        else
        {
            // make sure we load to a chunk boundary
            m_off_t endPos = ChunkedHash::chunkceil(startPos + *length, fain->size);
            *length = endPos - startPos;
            if (adjustsizeonly)
            {
                // return non-null to indicate success.  As it's a string return in the standard case, caller must deallocate as usual.
                return MegaApi::strdup("1");
            }
            else
            {
                auto localencryptedfilename = LocalPath::fromAbsolutePath(outputFilepath);

                std::unique_ptr<FileAccess> faout(api->fsAccess->newfileaccess());
                if (faout->fopen(localencryptedfilename, OPEN_WRONLY, FSLogging::logOnError))
                {
                    SymmCipher cipher;
                    cipher.setkey(filekey);
                    uint64_t ctriv = MemAccess::get<uint64_t>((const char*)filekey + SymmCipher::KEYLENGTH);

                    EncryptFilePieceByChunks ef(fain.get(), startPos, faout.get(), 0, &cipher, &chunkmacs, ctriv);
                    string urlSuffix;
                    if (ef.encrypt(startPos, endPos, urlSuffix))
                    {
                        ((int64_t*)filekey)[3] = chunkmacs.macsmac(&cipher);
                        return MegaApi::strdup(urlSuffix.c_str());
                    }
                }
            }
        }
    }
    return nullptr;
}

char *MegaBackgroundMediaUploadPrivate::getUploadURL()
{
    return url.empty() ? nullptr : MegaApi::strdup(url.c_str());
}

EncryptFilePieceByChunks::EncryptFilePieceByChunks(FileAccess *cFain, m_off_t cInPos, FileAccess *cFaout, m_off_t cOutPos,
                                                   SymmCipher *cipher, chunkmac_map *chunkmacs, uint64_t ctriv)
    : EncryptByChunks(cipher, chunkmacs, ctriv)
    , fain(cFain), faout(cFaout)
    , inpos(cInPos), outpos(cOutPos)
    , lastsize(0)
{
}

byte* EncryptFilePieceByChunks::nextbuffer(size_t bufsize)
{
    if (lastsize)
    {
        // write the last encrypted chunk
        if (!faout->fwrite((byte*)buffer.data(),
                           // EncryptByChunks writes one chunk at a time, bounded by ChunkedHash.
                           static_cast<unsigned>(lastsize),
                           outpos))
        {
            return NULL;
        }
        outpos += static_cast<m_off_t>(lastsize);
    }

    buffer.resize(bufsize + SymmCipher::BLOCKSIZE);
    memset((void*)(buffer.data() + bufsize), 0, SymmCipher::BLOCKSIZE);
    if (!fain->frawread((byte*)buffer.data(),
                        // EncryptByChunks reads one chunk at a time, bounded by ChunkedHash.
                        static_cast<unsigned>(bufsize),
                        inpos,
                        false,
                        FSLogging::logOnError))
    {
        return NULL;
    }
    lastsize = bufsize;
    inpos += static_cast<m_off_t>(bufsize);
    return (byte*)buffer.data();
}

/* BEGIN MEGAAPIIMPL */

#ifdef ENABLE_SYNC

int MegaApiImpl::isNodeSyncable(MegaNode *megaNode)
{
    MegaError *merror = isNodeSyncableWithError(megaNode);
    int r = merror->getErrorCode();
    delete merror;
    return r;
}

MegaError* MegaApiImpl::isNodeSyncableWithError(MegaNode* megaNode) {
    if (!megaNode)
    {
        return new MegaErrorPrivate(MegaError::API_EARGS);
    }

    SdkMutexGuard g(sdkMutex);
    shared_ptr<Node> node = client->nodebyhandle(megaNode->getHandle());
    if (!node)
    {
        return new MegaErrorPrivate(MegaError::API_ENOENT);
    }

    const auto [e, se] = client->isnodesyncable(node);
    return new MegaErrorPrivate(e, se);
}

bool MegaApiImpl::isScanning()
{
    return receivedScanningStateFlag.load();
}

bool MegaApiImpl::isSyncing()
{
    return receivedSyncingStateFlag.load();
}

MegaSync* MegaApiImpl::getSyncByBackupId(MegaHandle backupId)
{
    // syncs has its own thread safety
    SyncConfig config;
    if (client->syncs.syncConfigByBackupId(backupId, config))
    {
        return new MegaSyncPrivate(config, client);
    }
    return nullptr;
}

MegaSync *MegaApiImpl::getSyncByNode(MegaNode *node)
{
    if (!node)
    {
        return nullptr;
    }

    NodeHandle nodeHandle = NodeHandle().set6byte(node->getHandle());

    // syncs has its own thread safety
    for (auto& config : client->syncs.getConfigs(false))
    {
        if (config.mRemoteNode == nodeHandle)
        {
            return new MegaSyncPrivate(config, client);
        }
    }
    return nullptr;
}

MegaSync *MegaApiImpl::getSyncByPath(const char *localPath)
{
    if (!localPath)
    {
        return nullptr;
    }

    // syncs has its own thread safety
    for (auto& config : client->syncs.getConfigs(false))
    {
        if (config.getLocalPath().toPath(false) == localPath)
        {
            return new MegaSyncPrivate(config, client);
        }
    }
    return nullptr;
}

bool AddressedStallFilter::addressedNameConfict(const string& cloudPath, const LocalPath& localPath)
{
    lock_guard<mutex> g(m);

    if (!cloudPath.empty())
    {
        if (addressedNameConflictCloudStalls.find(cloudPath) != addressedNameConflictCloudStalls.end())
        {
            return true;
        }
    }
    if (!localPath.empty())
    {
        if (addressedNameConflictLocalStalls.find(localPath) != addressedNameConflictLocalStalls.end())
        {
            return true;
        }
    }
    return false;
}

bool AddressedStallFilter::addressedCloudStall(const string& cloudPath)
{
    assert (!cloudPath.empty());
    lock_guard<mutex> g(m);
    return addressedSyncCloudStalls.find(cloudPath) != addressedSyncCloudStalls.end();
}

bool AddressedStallFilter::addressedLocalStall(const LocalPath& localPath)
{
    assert (!localPath.empty());
    lock_guard<mutex> g(m);
    return addressedSyncLocalStalls.find(localPath) != addressedSyncLocalStalls.end();
}

void AddressedStallFilter::filterStallCloud(const string& cloudPath, int completedPassCount)
{
    lock_guard<mutex> g(m);
    addressedSyncCloudStalls[cloudPath] = completedPassCount;
}

void AddressedStallFilter::filterStallLocal(const LocalPath& localPath, int completedPassCount)
{
    lock_guard<mutex> g(m);
    addressedSyncLocalStalls[localPath] = completedPassCount;
}

void AddressedStallFilter::filterNameConfict(const string& cloudPath, const LocalPath& localPath, int completedPassCount)
{
    lock_guard<mutex> g(m);
    if (!cloudPath.empty())
    {
        addressedNameConflictCloudStalls[cloudPath] = completedPassCount;
    }
    if (!localPath.empty())
    {
        addressedNameConflictLocalStalls[localPath] = completedPassCount;
    }
}

void AddressedStallFilter::removeOldFilters(int completedPassCount)
{
    lock_guard<mutex> g(m);

    // when a filter was added, the sync code could already have started a new pass, and passed this node.
    // So, only after we are on a number greater than n+1 of the added n can we remove a filter

    for (auto i = addressedNameConflictLocalStalls.begin(); i != addressedNameConflictLocalStalls.end(); )
    {
        if (completedPassCount > i->second + 1)
        {
            i = addressedNameConflictLocalStalls.erase(i);
        }
        else ++i;
    }
    for (auto i = addressedNameConflictCloudStalls.begin(); i != addressedNameConflictCloudStalls.end(); )
    {
        if (completedPassCount > i->second + 1)
        {
            i = addressedNameConflictCloudStalls.erase(i);
        }
        else ++i;
    }
    for (auto i = addressedSyncLocalStalls.begin(); i != addressedSyncLocalStalls.end(); )
    {
        if (completedPassCount > i->second + 1)
        {
            i = addressedSyncLocalStalls.erase(i);
        }
        else ++i;
    }
    for (auto i = addressedSyncCloudStalls.begin(); i != addressedSyncCloudStalls.end(); )
    {
        if (completedPassCount > i->second + 1)
        {
            i = addressedSyncCloudStalls.erase(i);
        }
        else ++i;
    }
}

void AddressedStallFilter::clear()
{
    lock_guard<mutex> g(m);

    addressedSyncCloudStalls.clear();
    addressedSyncLocalStalls.clear();
    addressedNameConflictCloudStalls.clear();
    addressedNameConflictLocalStalls.clear();
}

void MegaApiImpl::getMegaSyncStallList(MegaRequestListener* listener)
{
    auto request = new MegaRequestPrivate(MegaRequest::TYPE_GET_SYNC_STALL_LIST, listener);

    request->performRequest = [this, request]() -> error
    {
        return performRequest_getSyncStalls(request);
    };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::getMegaSyncStallMap(MegaRequestListener* listener)
{
    auto request = new MegaRequestPrivate(MegaRequest::TYPE_GET_SYNC_STALL_LIST, listener);
    request->setFlag(true);

    request->performRequest = [this, request]() -> error
    {
        return performRequest_getSyncStalls(request);
    };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::clearStalledPath(MegaSyncStall* stall)
{
    // do not report these ones anymore in calls to getMegaSyncStallList
    // until the sync core has made another full pass over the sync tree

    if (auto ptr = dynamic_cast<MegaSyncStallPrivate*>(stall))
    {
        if (!ptr->info.cloudPath1.cloudPath.empty())
        {
            mAddressedStallFilter.filterStallCloud(ptr->info.cloudPath1.cloudPath,
                                                   client->syncs.completedPassCount.load());
        }
        if (!ptr->info.localPath1.localPath.empty())
        {
            mAddressedStallFilter.filterStallLocal(ptr->info.localPath1.localPath,
                                                   client->syncs.completedPassCount.load());
        }
    }
    else if (auto syncStall = dynamic_cast<MegaSyncNameConflictStallPrivate*>(stall))
    {
        mAddressedStallFilter.filterNameConfict(syncStall->mConflict.cloudPath,
                                                syncStall->mConflict.localPath,
                                                client->syncs.completedPassCount.load());
    }
}

void MegaApiImpl::moveToDebris(const char* path, MegaHandle syncBackupId, MegaRequestListener* listener)
{
    MegaRequestPrivate* request = new MegaRequestPrivate(MegaRequest::TYPE_MOVE_TO_DEBRIS, listener);

    request->setText(path);
    request->setNodeHandle(syncBackupId);
    request->performRequest = [this, request]()
    {
        const char* path = request->getText();
        handle syncBackupId = request->getNodeHandle();
        if (!path || syncBackupId == UNDEF)
        {
            return API_EARGS;
        }

        client->syncs.moveToSyncDebrisByBackupID(path, syncBackupId, nullptr, [this, request](error e)
                                                 {
                                                     fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e));
                                                 });

        return API_OK;
    };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::changeSyncRemoteRoot(const MegaHandle syncBackupId,
                                       const MegaHandle newRootNodeHandle,
                                       MegaRequestListener* listener)
{
    MegaRequestPrivate* request =
        new MegaRequestPrivate(MegaRequest::TYPE_CHANGE_SYNC_ROOT, listener);

    request->setNodeHandle(syncBackupId);
    request->setParentHandle(newRootNodeHandle);
    request->performRequest = [this, request]()
    {
        handle syncBackupId = request->getNodeHandle();
        handle newRootNodeHandle = request->getParentHandle();
        if (newRootNodeHandle == UNDEF || syncBackupId == UNDEF)
        {
            return API_EARGS;
        }
        client->changeSyncRoot(syncBackupId,
                               newRootNodeHandle,
                               nullptr,
                               [this, request](error e, SyncError se)
                               {
                                   fireOnRequestFinish(request,
                                                       std::make_unique<MegaErrorPrivate>(e, se));
                               });
        return API_OK;
    };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::changeSyncLocalRoot(const MegaHandle syncBackupId,
                                      const char* newLocalSyncRootPath,
                                      MegaRequestListener* listener)
{
    MegaRequestPrivate* request =
        new MegaRequestPrivate(MegaRequest::TYPE_CHANGE_SYNC_ROOT, listener);

    request->setNodeHandle(syncBackupId);
    request->setFile(newLocalSyncRootPath);
    request->performRequest = [this, request]()
    {
        handle syncBackupId = request->getNodeHandle();
        const char* newRootPath = request->getFile();
        if (syncBackupId == UNDEF || !newRootPath || newRootPath[0] == '\0')
        {
            return API_EARGS;
        }
        client->changeSyncRoot(syncBackupId,
                               UNDEF,
                               newRootPath,
                               [this, request](error e, SyncError se)
                               {
                                   fireOnRequestFinish(request,
                                                       std::make_unique<MegaErrorPrivate>(e, se));
                               });
        return API_OK;
    };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::setSyncUploadThrottleUpdateRate(const unsigned updateRateInSeconds,
                                                  MegaRequestListener* const listener)
{
    MegaRequestPrivate* const request =
        new MegaRequestPrivate(MegaRequest::TYPE_SET_SYNC_UPLOAD_THROTTLE_VALUES, listener);

    request->setNumber(updateRateInSeconds);
    request->performRequest = [this, request]()
    {
        const auto updateRateInSeconds = static_cast<unsigned>(request->getNumber());
        client->setSyncUploadThrottleUpdateRate(
            std::chrono::seconds(updateRateInSeconds),
            [this, request](const error errorSetUpdateRate)
            {
                fireOnRequestFinish(request,
                                    std::make_unique<MegaErrorPrivate>(errorSetUpdateRate));
            });
        return API_OK;
    };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::setSyncMaxUploadsBeforeThrottle(const unsigned maxUploadsBeforeThrottle,
                                                  MegaRequestListener* const listener)
{
    MegaRequestPrivate* const request =
        new MegaRequestPrivate(MegaRequest::TYPE_SET_SYNC_UPLOAD_THROTTLE_VALUES, listener);

    request->setTotalBytes(maxUploadsBeforeThrottle);
    request->performRequest = [this, request]()
    {
        const auto maxUploadsBeforeThrottle = static_cast<unsigned>(request->getTotalBytes());
        client->setSyncMaxUploadsBeforeThrottle(
            maxUploadsBeforeThrottle,
            [this, request](const error errorSetMaxUploadsBeforeThrottle)
            {
                fireOnRequestFinish(
                    request,
                    std::make_unique<MegaErrorPrivate>(errorSetMaxUploadsBeforeThrottle));
            });
        return API_OK;
    };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::getSyncUploadThrottleValues(MegaRequestListener* const listener)
{
    MegaRequestPrivate* const request =
        new MegaRequestPrivate(MegaRequest::TYPE_GET_SYNC_UPLOAD_THROTTLE_VALUES, listener);

    request->performRequest = [this, request]()
    {
        client->syncUploadThrottleValues(
            [this, request](const std::chrono::seconds updateRateInSeconds,
                            const unsigned maxUploadsBeforeThrottle)
            {
                request->setNumber(updateRateInSeconds.count());
                request->setTotalBytes(maxUploadsBeforeThrottle);
                fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(API_OK));
            });
        return API_OK;
    };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::getSyncUploadThrottleLimits(const bool upperLimits,
                                              MegaRequestListener* const listener)
{
    MegaRequestPrivate* const request =
        new MegaRequestPrivate(MegaRequest::TYPE_GET_SYNC_UPLOAD_THROTTLE_LIMITS, listener);

    request->setFlag(upperLimits);
    request->performRequest = [this, request]()
    {
        client->syncUploadThrottleValuesLimits(
            [this, request](ThrottleValueLimits&& throttleValueLimits)
            {
                const auto getUpperLimits = request->getFlag();
                request->setNumber(getUpperLimits ?
                                       throttleValueLimits.throttleUpdateRateUpperLimit.count() :
                                       throttleValueLimits.throttleUpdateRateLowerLimit.count());
                request->setTotalBytes(getUpperLimits ?
                                           throttleValueLimits.maxUploadsBeforeThrottleUpperLimit :
                                           throttleValueLimits.maxUploadsBeforeThrottleLowerLimit);
                fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(API_OK));
            });
        return API_OK;
    };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::checkSyncUploadsThrottled(MegaRequestListener* const listener)
{
    MegaRequestPrivate* const request =
        new MegaRequestPrivate(MegaRequest::TYPE_CHECK_SYNC_UPLOAD_THROTTLED_ELEMENTS, listener);

    request->performRequest = [this, request]()
    {
        client->checkSyncUploadsThrottled(
            [this, request](const bool throttledElements)
            {
                request->setFlag(throttledElements);
                fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(API_OK));
            });
        return API_OK;
    };

    requestQueue.push(request);
    waiter->notify();
}

MegaSyncStallPrivate::MegaSyncStallPrivate(const SyncStallEntry& e)
:info(e)
{}

MegaSyncStallPrivate* MegaSyncStallPrivate::copy() const
{
    return new MegaSyncStallPrivate(*this);
}

size_t MegaSyncStallPrivate::getHash() const
{
    if (!hashCache.first)
        hashCache = {true, std::hash<MegaSyncStallPrivate>{}(*this)};
    return hashCache.second;
}

const char*
MegaSyncStallPrivate::reasonDebugString(MegaSyncStall::SyncStallReason reason)
{
    static_assert(static_cast<int>(SyncWaitReason::NoReason) ==
                      static_cast<int>(MegaSyncStall::SyncStallReason::NoReason),
                  "");
    static_assert(static_cast<int>(SyncWaitReason::FileIssue) ==
                      static_cast<int>(MegaSyncStall::SyncStallReason::FileIssue),
                  "");
    static_assert(static_cast<int>(SyncWaitReason::MoveOrRenameCannotOccur) ==
                      static_cast<int>(MegaSyncStall::SyncStallReason::MoveOrRenameCannotOccur),
                  "");
    static_assert(
        static_cast<int>(SyncWaitReason::DeleteOrMoveWaitingOnScanning) ==
            static_cast<int>(MegaSyncStall::SyncStallReason::DeleteOrMoveWaitingOnScanning),
        "");
    static_assert(static_cast<int>(SyncWaitReason::DeleteWaitingOnMoves) ==
                      static_cast<int>(MegaSyncStall::SyncStallReason::DeleteWaitingOnMoves),
                  "");
    static_assert(static_cast<int>(SyncWaitReason::UploadIssue) ==
                      static_cast<int>(MegaSyncStall::SyncStallReason::UploadIssue),
                  "");
    static_assert(static_cast<int>(SyncWaitReason::DownloadIssue) ==
                      static_cast<int>(MegaSyncStall::SyncStallReason::DownloadIssue),
                  "");
    static_assert(static_cast<int>(SyncWaitReason::CannotCreateFolder) ==
                      static_cast<int>(MegaSyncStall::SyncStallReason::CannotCreateFolder),
                  "");
    static_assert(static_cast<int>(SyncWaitReason::CannotPerformDeletion) ==
                      static_cast<int>(MegaSyncStall::SyncStallReason::CannotPerformDeletion),
                  "");
    static_assert(
        static_cast<int>(SyncWaitReason::SyncItemExceedsSupportedTreeDepth) ==
            static_cast<int>(MegaSyncStall::SyncStallReason::SyncItemExceedsSupportedTreeDepth),
        "");
    static_assert(static_cast<int>(SyncWaitReason::FolderMatchedAgainstFile) ==
                      static_cast<int>(MegaSyncStall::SyncStallReason::FolderMatchedAgainstFile),
                  "");
    static_assert(
        static_cast<int>(
            SyncWaitReason::LocalAndRemoteChangedSinceLastSyncedState_userMustChoose) ==
            static_cast<int>(MegaSyncStall::SyncStallReason::
                                 LocalAndRemoteChangedSinceLastSyncedState_userMustChoose),
        "");
    static_assert(
        static_cast<int>(SyncWaitReason::LocalAndRemotePreviouslyUnsyncedDiffer_userMustChoose) ==
            static_cast<int>(MegaSyncStall::SyncStallReason::
                                 LocalAndRemotePreviouslyUnsyncedDiffer_userMustChoose),
        "");
    static_assert(static_cast<int>(SyncWaitReason::NamesWouldClashWhenSynced) ==
                      static_cast<int>(MegaSyncStall::SyncStallReason::NamesWouldClashWhenSynced),
                  "");
    static_assert(static_cast<int>(SyncWaitReason::SyncWaitReason_LastPlusOne) ==
                      static_cast<int>(MegaSyncStall::SyncStallReason::SyncStallReason_LastPlusOne),
                  "");

    return syncWaitReasonDebugString(SyncWaitReason(reason));
}

const char* MegaSyncStallPrivate::pathProblemDebugString(MegaSyncStall::SyncPathProblem reason)
{
    static_assert(static_cast<int>(PathProblem::NoProblem) ==
                      static_cast<int>(MegaSyncStall::SyncPathProblem::NoProblem),
                  "");

    static_assert(static_cast<int>(PathProblem::FileChangingFrequently) ==
                      static_cast<int>(MegaSyncStall::SyncPathProblem::FileChangingFrequently),
                  "");
    static_assert(static_cast<int>(PathProblem::IgnoreRulesUnknown) ==
                      static_cast<int>(MegaSyncStall::SyncPathProblem::IgnoreRulesUnknown),
                  "");
    static_assert(static_cast<int>(PathProblem::DetectedHardLink) ==
                      static_cast<int>(MegaSyncStall::SyncPathProblem::DetectedHardLink),
                  "");
    static_assert(static_cast<int>(PathProblem::DetectedSymlink) ==
                      static_cast<int>(MegaSyncStall::SyncPathProblem::DetectedSymlink),
                  "");
    static_assert(static_cast<int>(PathProblem::DetectedSpecialFile) ==
                      static_cast<int>(MegaSyncStall::SyncPathProblem::DetectedSpecialFile),
                  "");
    static_assert(
        static_cast<int>(PathProblem::DifferentFileOrFolderIsAlreadyPresent) ==
            static_cast<int>(MegaSyncStall::SyncPathProblem::DifferentFileOrFolderIsAlreadyPresent),
        "");
    static_assert(static_cast<int>(PathProblem::ParentFolderDoesNotExist) ==
                      static_cast<int>(MegaSyncStall::SyncPathProblem::ParentFolderDoesNotExist),
                  "");
    static_assert(
        static_cast<int>(PathProblem::FilesystemErrorDuringOperation) ==
            static_cast<int>(MegaSyncStall::SyncPathProblem::FilesystemErrorDuringOperation),
        "");
    static_assert(static_cast<int>(PathProblem::NameTooLongForFilesystem) ==
                      static_cast<int>(MegaSyncStall::SyncPathProblem::NameTooLongForFilesystem),
                  "");
    static_assert(static_cast<int>(PathProblem::CannotFingerprintFile) ==
                      static_cast<int>(MegaSyncStall::SyncPathProblem::CannotFingerprintFile),
                  "");
    static_assert(
        static_cast<int>(PathProblem::DestinationPathInUnresolvedArea) ==
            static_cast<int>(MegaSyncStall::SyncPathProblem::DestinationPathInUnresolvedArea),
        "");
    static_assert(static_cast<int>(PathProblem::MACVerificationFailure) ==
                      static_cast<int>(MegaSyncStall::SyncPathProblem::MACVerificationFailure),
                  "");
    static_assert(static_cast<int>(PathProblem::UnknownDownloadIssue) ==
                      static_cast<int>(MegaSyncStall::SyncPathProblem::UnknownDownloadIssue),
                  "");
    static_assert(static_cast<int>(PathProblem::DeletedOrMovedByUser) ==
                      static_cast<int>(MegaSyncStall::SyncPathProblem::DeletedOrMovedByUser),
                  "");
    static_assert(static_cast<int>(PathProblem::FileFolderDeletedByUser) ==
                      static_cast<int>(MegaSyncStall::SyncPathProblem::FileFolderDeletedByUser),
                  "");
    static_assert(static_cast<int>(PathProblem::MoveToDebrisFolderFailed) ==
                      static_cast<int>(MegaSyncStall::SyncPathProblem::MoveToDebrisFolderFailed),
                  "");
    static_assert(static_cast<int>(PathProblem::IgnoreFileMalformed) ==
                      static_cast<int>(MegaSyncStall::SyncPathProblem::IgnoreFileMalformed),
                  "");
    static_assert(
        static_cast<int>(PathProblem::FilesystemErrorListingFolder) ==
            static_cast<int>(MegaSyncStall::SyncPathProblem::FilesystemErrorListingFolder),
        "");
    static_assert(
        static_cast<int>(PathProblem::WaitingForScanningToComplete) ==
            static_cast<int>(MegaSyncStall::SyncPathProblem::WaitingForScanningToComplete),
        "");
    static_assert(
        static_cast<int>(PathProblem::WaitingForAnotherMoveToComplete) ==
            static_cast<int>(MegaSyncStall::SyncPathProblem::WaitingForAnotherMoveToComplete),
        "");
    static_assert(static_cast<int>(PathProblem::SourceWasMovedElsewhere) ==
                      static_cast<int>(MegaSyncStall::SyncPathProblem::SourceWasMovedElsewhere),
                  "");
    static_assert(
        static_cast<int>(PathProblem::FilesystemCannotStoreThisName) ==
            static_cast<int>(MegaSyncStall::SyncPathProblem::FilesystemCannotStoreThisName),
        "");
    static_assert(static_cast<int>(PathProblem::CloudNodeInvalidFingerprint) ==
                      static_cast<int>(MegaSyncStall::SyncPathProblem::CloudNodeInvalidFingerprint),
                  "");
    static_assert(static_cast<int>(PathProblem::PutnodeDeferredByController) ==
                      static_cast<int>(MegaSyncStall::SyncPathProblem::PutnodeDeferredByController),
                  "");
    static_assert(
        static_cast<int>(PathProblem::PutnodeCompletionDeferredByController) ==
            static_cast<int>(MegaSyncStall::SyncPathProblem::PutnodeCompletionDeferredByController),
        "");
    static_assert(static_cast<int>(PathProblem::PutnodeCompletionPending) ==
                      static_cast<int>(MegaSyncStall::SyncPathProblem::PutnodeCompletionPending),
                  "");
    static_assert(static_cast<int>(PathProblem::UploadDeferredByController) ==
                      static_cast<int>(MegaSyncStall::SyncPathProblem::UploadDeferredByController),
                  "");
    static_assert(static_cast<int>(PathProblem::DetectedNestedMount) ==
                      static_cast<int>(MegaSyncStall::SyncPathProblem::DetectedNestedMount),
                  "");
    static_assert(static_cast<int>(PathProblem::CloudNodeIsBlocked) ==
                      static_cast<int>(MegaSyncStall::SyncPathProblem::CloudNodeIsBlocked),
                  "");
    static_assert(static_cast<int>(PathProblem::PathProblem_LastPlusOne) ==
                      static_cast<int>(MegaSyncStall::SyncPathProblem::SyncPathProblem_LastPlusOne),
                  "");

    return syncPathProblemDebugString(PathProblem(reason));
}

size_t MegaSyncNameConflictStallPrivate::getHash() const
{
    if (!hashCache.first)
        hashCache = {true, std::hash<MegaSyncNameConflictStallPrivate>{}(*this)};
    return hashCache.second;
}

const char*
MegaSyncNameConflictStallPrivate::reasonDebugString(MegaSyncStall::SyncStallReason reason)
{
    return syncWaitReasonDebugString(SyncWaitReason(reason));
}

const char*
MegaSyncNameConflictStallPrivate::pathProblemDebugString(MegaSyncStall::SyncPathProblem reason)
{
    return syncPathProblemDebugString(PathProblem(reason));
}

MegaSyncStallListPrivate* MegaSyncStallListPrivate::copy() const {
    return new MegaSyncStallListPrivate(*this);
}

const MegaSyncStall* MegaSyncStallListPrivate::get(size_t i) const
{
    if( i < mStalls.size())
    {
        return mStalls[i].get();
    }

    return nullptr;
}

MegaSyncStallListPrivate::MegaSyncStallListPrivate(SyncProblems&& sp, AddressedStallFilter& filter)
{
    for (auto& itnc: sp.mConflictsMap)
    {
        for (auto& nc: itnc.second)
        {
            if (!filter.addressedNameConfict(nc.cloudPath, nc.localPath))
            {
                mStalls.push_back(std::make_shared<MegaSyncNameConflictStallPrivate>(nc));
            }
        }
    }

    for (auto& stalledSyncMapPair : sp.mStalls.syncStallInfoMaps)
    {
        auto& stalledSyncMap = stalledSyncMapPair.second;
        for(auto& stall : stalledSyncMap.cloud)
        {
            if (!filter.addressedCloudStall(stall.first))
            {
                mStalls.push_back(std::make_shared<MegaSyncStallPrivate>(stall.second));
            }
        }

        for(auto& stall : stalledSyncMap.local)
        {
            if (!filter.addressedLocalStall(stall.first))
            {
                mStalls.push_back(std::make_shared<MegaSyncStallPrivate>(stall.second));
            }
        }
    }
}

MegaSyncStallMapPrivate::MegaSyncStallMapPrivate(SyncProblems&& sp, AddressedStallFilter& filter)
{
    for (const auto& [syncId, nameConflictList]: sp.mConflictsMap)
    {
        auto& stallList = mStallsMap[syncId];
        for (const auto& nc: nameConflictList)
        {
            if (!filter.addressedNameConfict(nc.cloudPath, nc.localPath))
            {
                stallList.addStall(std::make_shared<MegaSyncNameConflictStallPrivate>(nc));
            }
        }
    }

    for (const auto& [syncId, stalledSyncMap]: sp.mStalls.syncStallInfoMaps)
    {
        auto addStalls =
            [&stallList = mStallsMap[syncId], &filter](const auto& stallMap, auto filterFunc)
        {
            for (const auto& stall: stallMap)
            {
                if (!std::invoke(filterFunc, filter, stall.first))
                {
                    stallList.addStall(std::make_shared<MegaSyncStallPrivate>(stall.second));
                }
            }
        };

        addStalls(stalledSyncMap.cloud, &AddressedStallFilter::addressedCloudStall);
        addStalls(stalledSyncMap.local, &AddressedStallFilter::addressedLocalStall);
    }
}

MegaHandleList* MegaSyncStallMapPrivate::getKeys() const
{
    MegaHandleList* list = MegaHandleList::createInstance();
    for (const auto& stall: mStallsMap)
    {
        list->addMegaHandle(stall.first);
    }
    return list;
}

const std::map<MegaHandle, MegaSyncStallListPrivate>& MegaSyncStallMapPrivate::getMap() const
{
    return mStallsMap;
}
#endif // ENABLE_SYNC

MegaScheduledCopy *MegaApiImpl::getScheduledCopyByTag(int tag)
{
    SdkMutexGuard g(sdkMutex);
    if (backupsMap.find(tag) == backupsMap.end())
    {
        return NULL;
    }
    return backupsMap.at(tag)->copy();
}

MegaScheduledCopy *MegaApiImpl::getScheduledCopyByNode(MegaNode *node)
{
    if (!node)
    {
        return NULL;
    }

    MegaScheduledCopy *result = NULL;
    MegaHandle nodeHandle = node->getHandle();
    SdkMutexGuard g(sdkMutex);
    std::map<int, MegaScheduledCopyController*>::iterator it = backupsMap.begin();
    while(it != backupsMap.end())
    {
        MegaScheduledCopyController* backup = it->second;
        if (backup->getMegaHandle() == nodeHandle)
        {
            result = backup->copy();
            break;
        }
        it++;
    }
    return result;
}

MegaScheduledCopy *MegaApiImpl::getScheduledCopyByPath(const char *localPath)
{
    if (!localPath)
    {
        return NULL;
    }

    MegaScheduledCopy *result = NULL;
    SdkMutexGuard g(sdkMutex);
    std::map<int, MegaScheduledCopyController*>::iterator it = backupsMap.begin();
    while(it != backupsMap.end())
    {
        MegaScheduledCopyController* backup = it->second;
        if (!strcmp(localPath, backup->getLocalFolder()))
        {
            result = backup->copy();
            break;
        }
        it++;
    }
    return result;
}
bool MegaNodePrivate::hasThumbnail()
{
    return thumbnailAvailable;
}

bool MegaNodePrivate::hasPreview()
{
    return previewAvailable;
}

bool MegaNodePrivate::isPublic()
{
    return isPublicNode;
}

bool MegaNodePrivate::isShared()
{
    return outShares || inShare;
}

bool MegaNodePrivate::isOutShare()
{
    return outShares;
}

bool MegaNodePrivate::isInShare()
{
    return inShare;
}

bool MegaNodePrivate::isExported()
{
    return plink != nullptr;
}

bool MegaNodePrivate::isExpired()
{
    return plink ? (plink->isExpired()) : false;
}

bool MegaNodePrivate::isTakenDown()
{
    return plink ? plink->takendown : false;
}

bool MegaNodePrivate::isForeign()
{
    return foreign;
}

bool MegaNodePrivate::isCreditCardNode() const
{
    if (!isPasswordManagerNode())
        return false;

    const auto passNodeAttr = getOfficialAttr(MegaClient::NODE_ATTR_PASSWORD_MANAGER);
    assert(passNodeAttr);

    AttrMap attrMap;
    attrMap.fromjson(passNodeAttr);
    return MegaClient::isPwmDataOfType(attrMap, MegaClient::PwmEntryType::CREDIT_CARD);
}

bool MegaNodePrivate::isPasswordNode() const
{
    if (!isPasswordManagerNode())
        return false;

    const auto passNodeAttr = getOfficialAttr(MegaClient::NODE_ATTR_PASSWORD_MANAGER);
    assert(passNodeAttr);

    AttrMap attrMap;
    attrMap.fromjson(passNodeAttr);
    return MegaClient::isPwmDataOfType(attrMap, MegaClient::PwmEntryType::PASSWORD);
}

bool MegaNodePrivate::isPasswordManagerNode() const
{
    return (type == FOLDERNODE &&
            getOfficialAttr(MegaClient::NODE_ATTR_PASSWORD_MANAGER) != nullptr);
}

MegaNode::CreditCardNodeData* MegaNodePrivate::getCreditCardData() const
{
    if (!isCreditCardNode())
        return nullptr;

    AttrMap aux;
    aux.fromjson(getOfficialAttr(MegaClient::NODE_ATTR_PASSWORD_MANAGER));

    return new CCNDataPrivate{
        getConstCharPtr(aux.getString(MegaClient::PWM_ATTR_CREDIT_CARD_NUMBER)),
        getConstCharPtr(aux.getString(MegaClient::PWM_ATTR_CREDIT_NOTES)),
        getConstCharPtr(aux.getString(MegaClient::PWM_ATTR_CREDIT_CARD_HOLDER)),
        getConstCharPtr(aux.getString(MegaClient::PWM_ATTR_CREDIT_CVV)),
        getConstCharPtr(aux.getString(MegaClient::PWM_ATTR_CREDIT_EXP_DATE))};
}

MegaNode::PasswordNodeData* MegaNodePrivate::getPasswordData() const
{
    if (isPasswordNode())
    {
        AttrMap aux;
        aux.fromjson(getOfficialAttr(MegaClient::NODE_ATTR_PASSWORD_MANAGER));
        constexpr auto totpNameid = AttrMap::string2nameid(MegaClient::PWM_ATTR_PASSWORD_TOTP);

        std::optional<PNDataPrivate::TotpDataPrivate> totp{};
        if (aux.map.contains(totpNameid))
        {
            AttrMap auxTotp;
            auxTotp.fromjsonObject(
                aux.map.at(AttrMap::string2nameid(MegaClient::PWM_ATTR_PASSWORD_TOTP)));
            totp = PNDataPrivate::TotpDataPrivate::fromMap(auxTotp);
        }

        return new PNDataPrivate{
            getConstCharPtr(aux.getString(MegaClient::PWM_ATTR_PASSWORD_PWD)),
            getConstCharPtr(aux.getString(MegaClient::PWM_ATTR_PASSWORD_NOTES)),
            getConstCharPtr(aux.getString(MegaClient::PWM_ATTR_PASSWORD_URL)),
            getConstCharPtr(aux.getString(MegaClient::PWM_ATTR_PASSWORD_USERNAME)),
            getPtr(totp)};
    }

    return nullptr;
}

string *MegaNodePrivate::getPrivateAuth()
{
    return &privateAuth;
}

MegaNodeList *MegaNodePrivate::getChildren()
{
    return children;
}

void MegaNodePrivate::setPrivateAuth(const char* newPrivateAuth)
{
    if (!newPrivateAuth || !newPrivateAuth[0])
    {
        privateAuth.clear();
    }
    else
    {
        privateAuth = newPrivateAuth;
    }
}

void MegaNodePrivate::setPublicAuth(const char* newPublicAuth)
{
    if (!newPublicAuth || !newPublicAuth[0])
    {
        publicAuth.clear();
    }
    else
    {
        publicAuth = newPublicAuth;
    }
}

void MegaNodePrivate::setChatAuth(const char* newChatAuth)
{
    delete[] chatAuth;
    if (!newChatAuth || !newChatAuth[0])
    {
        chatAuth = NULL;
        foreign = false;
    }
    else
    {
        chatAuth = MegaApi::strdup(newChatAuth);
        foreign = true;
    }
}

void MegaNodePrivate::setForeign(bool isForeign)
{
    foreign = isForeign;
}

void MegaNodePrivate::setChildren(MegaNodeList* newChildren)
{
    children = newChildren;
}

void MegaNodePrivate::setName(const char *newName)
{
    if (name)
        delete [] name;

    name = MegaApi::strdup(newName);
}

string *MegaNodePrivate::getPublicAuth()
{
    return &publicAuth;
}

const char *MegaNodePrivate::getChatAuth()
{
    return chatAuth;
}

MegaNodePrivate::~MegaNodePrivate()
{
    delete[] name;
    delete[] fingerprint;
    delete[] originalfingerprint;
    delete [] chatAuth;
    delete customAttrs;
    delete plink;
    delete sharekey;
    delete children;
}

MegaUserPrivate::MegaUserPrivate(User *user) : MegaUser()
{
    email = MegaApi::strdup(user->email.c_str());
    handle = user->userhandle;
    visibility = user->show;
    ctime = user->ctime;
    tag = user->getTag();
    changed = 0;
    if (user->changed.authring)
    {
        changed |= MegaUser::CHANGE_TYPE_AUTHRING;
    }
    if(user->changed.avatar)
    {
        changed |= MegaUser::CHANGE_TYPE_AVATAR;
    }
    if(user->changed.lstint)
    {
        changed |= MegaUser::CHANGE_TYPE_LSTINT;
    }
    if(user->changed.firstname)
    {
        changed |= MegaUser::CHANGE_TYPE_FIRSTNAME;
    }
    if(user->changed.lastname)
    {
        changed |= MegaUser::CHANGE_TYPE_LASTNAME;
    }
    if(user->changed.email)
    {
        changed |= MegaUser::CHANGE_TYPE_EMAIL;
    }
    if(user->changed.keyring)
    {
        changed |= MegaUser::CHANGE_TYPE_KEYRING;
    }
    if(user->changed.country)
    {
        changed |= MegaUser::CHANGE_TYPE_COUNTRY;
    }
    if(user->changed.birthday)
    {
        changed |= MegaUser::CHANGE_TYPE_BIRTHDAY;
    }
    if(user->changed.puCu255)
    {
        changed |= MegaUser::CHANGE_TYPE_PUBKEY_CU255;
    }
    if(user->changed.puEd255)
    {
        changed |= MegaUser::CHANGE_TYPE_PUBKEY_ED255;
    }
    if(user->changed.sigPubk)
    {
        changed |= MegaUser::CHANGE_TYPE_SIG_PUBKEY_RSA;
    }
    if(user->changed.sigCu255)
    {
        changed |= MegaUser::CHANGE_TYPE_SIG_PUBKEY_CU255;
    }
    if(user->changed.language)
    {
        changed |= MegaUser::CHANGE_TYPE_LANGUAGE;
    }
    if(user->changed.pwdReminder)
    {
        changed |= MegaUser::CHANGE_TYPE_PWD_REMINDER;
    }
    if(user->changed.disableVersions)
    {
        changed |= MegaUser::CHANGE_TYPE_DISABLE_VERSIONS;
    }
    if(user->changed.noCallKit)
    {
        changed |= MegaUser::CHANGE_TYPE_NO_CALLKIT;
    }
    if(user->changed.contactLinkVerification)
    {
        changed |= MegaUser::CHANGE_TYPE_CONTACT_LINK_VERIFICATION;
    }
    if(user->changed.richPreviews)
    {
        changed |= MegaUser::CHANGE_TYPE_RICH_PREVIEWS;
    }
    if(user->changed.rubbishTime)
    {
        changed |= MegaUser::CHANGE_TYPE_RUBBISH_TIME;
    }
    if(user->changed.storageState)
    {
        changed |= MegaUser::CHANGE_TYPE_STORAGE_STATE;
    }
    if(user->changed.geolocation)
    {
        changed |= MegaUser::CHANGE_TYPE_GEOLOCATION;
    }
    if(user->changed.cameraUploadsFolder)
    {
        changed |= MegaUser::CHANGE_TYPE_CAMERA_UPLOADS_FOLDER;
    }
    if(user->changed.myChatFilesFolder)
    {
        changed |= MegaUser::CHANGE_TYPE_MY_CHAT_FILES_FOLDER;
    }
    if (user->changed.pushSettings)
    {
        changed |= MegaUser::CHANGE_TYPE_PUSH_SETTINGS;
    }
    if (user->changed.alias)
    {
        changed |= MegaUser::CHANGE_TYPE_ALIAS;
    }
    if (user->changed.unshareablekey)
    {
        changed |= MegaUser::CHANGE_TYPE_UNSHAREABLE_KEY;
    }
    if (user->changed.devicenames)
    {
        changed |= MegaUser::CHANGE_TYPE_DEVICE_NAMES;
    }
    if (user->changed.myBackupsFolder)
    {
        changed |= MegaUser::CHANGE_TYPE_MY_BACKUPS_FOLDER;
    }
    if (user->changed.cookieSettings)
    {
        changed |= MegaUser::CHANGE_TYPE_COOKIE_SETTINGS;
    }
    if (user->changed.aPrefs)
    {
        changed |= MegaUser::CHANGE_APPS_PREFS;
    }
    if (user->changed.ccPrefs)
    {
        changed |= MegaUser::CHANGE_CC_PREFS;
    }
    if (user->changed.recentClearTimestamp)
    {
        changed |= MegaUser::CHANGE_TYPE_RECENT_CLEAR_TIMESTAMP;
    }
    // Don't need to notify about user->changed.enableTestSurveys
}

MegaUserPrivate::MegaUserPrivate(MegaUser *user) : MegaUser()
{
    email = MegaApi::strdup(user->getEmail());
    handle = user->getHandle();
    visibility = user->getVisibility();
    ctime = user->getTimestamp();
    changed = user->getChanges();
    tag = user->isOwnChange();
}

MegaUser *MegaUserPrivate::fromUser(User *user)
{
    if(!user)
    {
        return NULL;
    }
    return new MegaUserPrivate(user);
}

MegaUser *MegaUserPrivate::copy()
{
    return new MegaUserPrivate(this);
}

MegaUserPrivate::~MegaUserPrivate()
{
    delete[] email;
}

const char* MegaUserPrivate::getEmail()
{
    return email;
}

MegaHandle MegaUserPrivate::getHandle()
{
    return handle;
}

int MegaUserPrivate::getVisibility()
{
    return visibility;
}

int64_t MegaUserPrivate::getTimestamp()
{
    return ctime;
}

bool MegaUserPrivate::hasChanged(uint64_t changeType)
{
    return (changed & changeType);
}

uint64_t MegaUserPrivate::getChanges()
{
    return changed;
}

int MegaUserPrivate::isOwnChange()
{
    return tag;
}

bool MegaSetPrivate::hasChanged(uint64_t changeType) const
{
    return getChanges() & changeType;
}

bool MegaSetElementPrivate::hasChanged(uint64_t changeType) const
{
    return getChanges() & changeType;
}

MegaUserAlertPrivate::MegaUserAlertPrivate(UserAlert::Base *b, MegaClient* mc)
    : id(b->id)
    , seen(b->seen())
    , relevant(b->relevant())
    , type(-1)
    , tag(b->tag)
    , userHandle(UNDEF)
    , nodeHandle(UNDEF)
    , removed(b->removed())
{
    b->text(heading, title, mc);
    timestamps.push_back(b->ts());

    switch (b->type)
    {
    case name_id::ipc:
    {
        UserAlert::IncomingPendingContact* p = static_cast<UserAlert::IncomingPendingContact*>(b);
        if (p->requestWasDeleted)
        {
            type = TYPE_INCOMINGPENDINGCONTACT_CANCELLED;
        }
        else if (p->requestWasReminded)
        {
            type = TYPE_INCOMINGPENDINGCONTACT_REMINDER;
        }
        else
        {
            type = TYPE_INCOMINGPENDINGCONTACT_REQUEST;
        }
        userHandle = p->user();
        mPcrHandle = p->mPcrHandle;
        email = p->email();
    }
    break;
    case name_id::c:
    {
        UserAlert::ContactChange* p = static_cast<UserAlert::ContactChange*>(b);
        switch (p->action)
        {
        case 0: type = TYPE_CONTACTCHANGE_DELETEDYOU; break;
        case 1: type = TYPE_CONTACTCHANGE_CONTACTESTABLISHED; break;
        case 2: type = TYPE_CONTACTCHANGE_ACCOUNTDELETED; break;
        case 3: type = TYPE_CONTACTCHANGE_BLOCKEDYOU; break;
        }
        userHandle = p->user();
        email = p->email();
    }
    break;
    case name_id::upci:
    {
        UserAlert::UpdatedPendingContactIncoming* p = static_cast<UserAlert::UpdatedPendingContactIncoming*>(b);
        switch (p->action)
        {
        case 1: type = TYPE_UPDATEDPENDINGCONTACTINCOMING_IGNORED; break;
        case 2: type = TYPE_UPDATEDPENDINGCONTACTINCOMING_ACCEPTED; break;
        case 3: type = TYPE_UPDATEDPENDINGCONTACTINCOMING_DENIED; break;
        }
        userHandle = p->user();
        email = p->email();
    }
    break;
    case name_id::upco:
    {
        UserAlert::UpdatedPendingContactOutgoing* p = static_cast<UserAlert::UpdatedPendingContactOutgoing*>(b);
        switch (p->action)
        {
        case 1: type = TYPE_UPDATEDPENDINGCONTACTINCOMING_IGNORED; break;
        case 2: type = TYPE_UPDATEDPENDINGCONTACTOUTGOING_ACCEPTED; break;
        case 3: type = TYPE_UPDATEDPENDINGCONTACTOUTGOING_DENIED; break;
        }
        userHandle = p->user();
        email = p->email();
    }
    break;
    case name_id::share:
    {
        UserAlert::NewShare* p = static_cast<UserAlert::NewShare*>(b);
        type = TYPE_NEWSHARE;
        userHandle = p->user();
        email = p->email();
        nodeHandle = p->folderhandle;
        if (shared_ptr<Node> node = mc->nodebyhandle(p->folderhandle))
        {
            nodePath = node->displaypath();
            nodeName = node->displayname();
        }
    }
    break;
    case name_id::dshare:
    {
        UserAlert::DeletedShare* p = static_cast<UserAlert::DeletedShare*>(b);
        type = TYPE_DELETEDSHARE;
        userHandle = p->user();
        email = p->email();
        nodePath = p->folderPath;
        nodeName = p->folderName;
        nodeHandle = p->folderHandle;
        bool accessRevoked = p->user() == p->ownerHandle;
        numbers.push_back(accessRevoked ? 1 : 0);
    }
    break;
    case name_id::put:
    {
        UserAlert::NewSharedNodes* p = static_cast<UserAlert::NewSharedNodes*>(b);
        type = TYPE_NEWSHAREDNODES;
        userHandle = p->user();
        email = p->email();
        nodeHandle = p->parentHandle;
        numbers.push_back(static_cast<int64_t>(p->folderNodeHandles.size()));
        numbers.push_back(static_cast<int64_t>(p->fileNodeHandles.size()));
        handles.assign(begin(p->folderNodeHandles), end(p->folderNodeHandles));
        handles.insert(end(handles), begin(p->fileNodeHandles), end(p->fileNodeHandles));
    }
    break;
    case name_id::d:
    {
        UserAlert::RemovedSharedNode* p = static_cast<UserAlert::RemovedSharedNode*>(b);
        type = TYPE_REMOVEDSHAREDNODES;
        userHandle = p->user();
        email = p->email();
        numbers.push_back(static_cast<int64_t>(p->nodeHandles.size()));
    }
    break;
    case name_id::u:
    {
        UserAlert::UpdatedSharedNode* p = static_cast<UserAlert::UpdatedSharedNode*>(b);
        type = TYPE_UPDATEDSHAREDNODES;
        userHandle = p->user();
        email = p->email();
        numbers.push_back(static_cast<int64_t>(p->nodeHandles.size()));
    }
    break;
    case name_id::psts:
    case name_id::psts_v2:
    {
        UserAlert::Payment* p = static_cast<UserAlert::Payment*>(b);
        type = p->success ? TYPE_PAYMENT_SUCCEEDED : TYPE_PAYMENT_FAILED;
        extraStrings.push_back(p->getProPlanName());
    }
    break;
    case name_id::pses:
    {
        UserAlert::PaymentReminder* p = static_cast<UserAlert::PaymentReminder*>(b);
        type = TYPE_PAYMENTREMINDER;
        timestamps.push_back(p->expiryTime);
    }
    break;
    case name_id::ph:
    {
        UserAlert::Takedown* p = static_cast<UserAlert::Takedown*>(b);
        if (p->isTakedown)
        {
            type = TYPE_TAKEDOWN;
        }
        else if (p->isReinstate)
        {
            type = TYPE_TAKEDOWN_REINSTATED;
        }
        nodeHandle = p->nodeHandle;
        shared_ptr<Node> node = mc->nodebyhandle(nodeHandle);
        if (node)
        {
            nodePath = node->displaypath();
            nodeName = node->displayname();
        }
    }
    break;
    case name_id::ass:
    {
        UserAlert::SetTakedown* p = static_cast<UserAlert::SetTakedown*>(b);
        if (p->isTakedown)
        {
            type = TYPE_SET_TAKEDOWN;
        }
        else if (p->isReinstate)
        {
            type = TYPE_SET_TAKEDOWN_REINSTATED;
        }
        nodeHandle = p->setId;
        if (const Set* set = mc->getSet(nodeHandle); set)
        {
            nodeName = set->name();
        }
    }
    break;
#ifdef ENABLE_CHAT
    case name_id::mcsmp:
    {
         if (auto* p = dynamic_cast<UserAlert::NewScheduledMeeting*>(b))
         {
             type = TYPE_SCHEDULEDMEETING_NEW;
             userHandle = p->user();
             email = p->email();
             nodeHandle = p->mChatid;
             schedMeetingId = p->mSchedMeetingHandle;
             mPcrHandle = p->mParentSchedId;
             numbers.push_back(p->mStartDateTime);
         }
         else
         {
             if (auto* updateAlert = dynamic_cast<UserAlert::UpdatedScheduledMeeting*>(b))
             {
                 type = TYPE_SCHEDULEDMEETING_UPDATED;
                 userHandle = updateAlert->user();
                 email = updateAlert->email();
                 nodeHandle = updateAlert->mChatid;
                 schedMeetingId = updateAlert->mSchedMeetingHandle;
                 mPcrHandle = updateAlert->mParentSchedId;
                 numbers.push_back(updateAlert->mStartDateTime);
                 schedMeetingChangeset = updateAlert->mUpdatedChangeset;
             }
             else
             {
                 assert(false);
                 LOG_err << "Scheduled meeting user alert invalid sub-type (mangled): "
                         << typeid(*b).name()
                         << ", expected: NewSchedulingMeeting or UpdatedSchedulingMeeting";
             }
         }
    }
    break;
    case name_id::mcsmr:
    {
        if (auto* p = dynamic_cast<UserAlert::DeletedScheduledMeeting*>(b))
        {
            type = TYPE_SCHEDULEDMEETING_DELETED;
            userHandle = p->user();
            email = p->email();
            nodeHandle = p->mChatid;
            schedMeetingId = p->mSchedMeetingHandle;
        }
        else
        {
            LOG_err << "Scheduled meeting user alert invalid sub-type (mangled): "
                    << typeid(*b).name()
                    << ", expected: DeletedScheduledMeeting";
        }
    }
    break;
#endif
    } // end switch
}

MegaUserAlert *MegaUserAlertPrivate::copy() const
{
    return new MegaUserAlertPrivate(*this);
}

unsigned MegaUserAlertPrivate::getId() const
{
    return id;
}

bool MegaUserAlertPrivate::getSeen() const
{
    return seen;
}

bool MegaUserAlertPrivate::getRelevant() const
{
    return relevant;
}

int MegaUserAlertPrivate::getType() const
{
    return type;
}

const char *MegaUserAlertPrivate::getTypeString() const
{
    switch (type)
    {
    case TYPE_INCOMINGPENDINGCONTACT_REQUEST:           return "NEW_CONTACT_REQUEST";
    case TYPE_INCOMINGPENDINGCONTACT_CANCELLED:         return "CONTACT_REQUEST_CANCELLED";
    case TYPE_INCOMINGPENDINGCONTACT_REMINDER:          return "CONTACT_REQUEST_REMINDED";
    case TYPE_CONTACTCHANGE_DELETEDYOU:                 return "CONTACT_DISCONNECTED";
    case TYPE_CONTACTCHANGE_CONTACTESTABLISHED:         return "CONTACT_ESTABLISHED";
    case TYPE_CONTACTCHANGE_ACCOUNTDELETED:             return "CONTACT_ACCOUNTDELETED";
    case TYPE_CONTACTCHANGE_BLOCKEDYOU:                 return "CONTACT_BLOCKED";
    case TYPE_UPDATEDPENDINGCONTACTINCOMING_IGNORED:    return "YOU_IGNORED_CONTACT";
    case TYPE_UPDATEDPENDINGCONTACTINCOMING_ACCEPTED:   return "YOU_ACCEPTED_CONTACT";
    case TYPE_UPDATEDPENDINGCONTACTINCOMING_DENIED:     return "YOU_DENIED_CONTACT";
    case TYPE_UPDATEDPENDINGCONTACTOUTGOING_ACCEPTED:   return "CONTACT_ACCEPTED_YOU";
    case TYPE_UPDATEDPENDINGCONTACTOUTGOING_DENIED:     return "CONTACT_DENIED_YOU";
    case TYPE_NEWSHARE:                                 return "NEW_SHARE";
    case TYPE_DELETEDSHARE:                             return "SHARE_UNSHARED";
    case TYPE_NEWSHAREDNODES:                           return "NEW_NODES_IN_SHARE";
    case TYPE_REMOVEDSHAREDNODES:                       return "NODES_IN_SHARE_REMOVED";
    case TYPE_UPDATEDSHAREDNODES:                       return "NODES_IN_SHARE_UPDATED";
    case TYPE_PAYMENT_SUCCEEDED:                        return "PAYMENT_SUCCEEDED";
    case TYPE_PAYMENT_FAILED:                           return "PAYMENT_FAILED";
    case TYPE_PAYMENTREMINDER:                          return "PAYMENT_REMINDER";
    case TYPE_TAKEDOWN:                                 return "TAKEDOWN";
    case TYPE_TAKEDOWN_REINSTATED:                      return "TAKEDOWN_REINSTATED";
    case TYPE_SCHEDULEDMEETING_NEW:                     return "SCHEDULEDMEETING_NEW";
    case TYPE_SCHEDULEDMEETING_UPDATED:                 return "SCHEDULEDMEETING_UPDATED";
    case TYPE_SCHEDULEDMEETING_DELETED:                 return "SCHEDULEDMEETING_DELETED";
    case TYPE_SET_TAKEDOWN:
        return "SET_TAKEDOWN";
    case TYPE_SET_TAKEDOWN_REINSTATED:
        return "SET_TAKEDOWN_REINSTATED";
    }
    return "<new type>";
}

MegaHandle MegaUserAlertPrivate::getUserHandle() const
{
    return userHandle;
}

MegaHandle MegaUserAlertPrivate::getNodeHandle() const
{
    return nodeHandle;
}

MegaHandle mega::MegaUserAlertPrivate::getPcrHandle() const
{
    return mPcrHandle;
}

const char* MegaUserAlertPrivate::getEmail() const
{
    return email.empty() ? NULL : email.c_str();
}

const char*MegaUserAlertPrivate::getPath() const
{
    return  nodePath.empty() ? NULL : nodePath.c_str();
}

const char *MegaUserAlertPrivate::getName() const
{
    return  nodeName.empty() ? NULL : nodeName.c_str();
}

const char *MegaUserAlertPrivate::getHeading() const
{
    return heading.c_str();
}

const char *MegaUserAlertPrivate::getTitle() const
{
    return title.c_str();
}

int64_t MegaUserAlertPrivate::getNumber(unsigned index) const
{
    return index < numbers.size() ? numbers[index] : -1;
}

int64_t MegaUserAlertPrivate::getTimestamp(unsigned index) const
{
    return index < timestamps.size() ? timestamps[index] : -1;
}

const char* MegaUserAlertPrivate::getString(unsigned index) const
{
    return index < extraStrings.size() ? extraStrings[index].c_str() : NULL;
}

MegaHandle MegaUserAlertPrivate::getHandle(unsigned index) const
{
    return index < handles.size() ? handles[index] : INVALID_HANDLE;
}

#ifdef ENABLE_CHAT
MegaHandle MegaUserAlertPrivate::getSchedId() const
{
    return schedMeetingId;
}

bool MegaUserAlertPrivate::hasSchedMeetingChanged(uint64_t changeType) const
{
    return schedMeetingChangeset.hasChanged(changeType);
}

MegaStringList* MegaUserAlertPrivate::getUpdatedTitle() const
{
    if (!hasSchedMeetingChanged(SM_CHANGE_TYPE_TITLE)
            || !schedMeetingChangeset.getUpdatedTitle())
    {
        return nullptr;
    }

    MegaStringList* updatedTitle = MegaStringList::createInstance();
    updatedTitle->add(Base64::atob(schedMeetingChangeset.getUpdatedTitle()->oldValue).c_str());
    updatedTitle->add(Base64::atob(schedMeetingChangeset.getUpdatedTitle()->newValue).c_str());
    return updatedTitle;
}

MegaStringList* MegaUserAlertPrivate::getUpdatedTimeZone() const
{
    if (!hasSchedMeetingChanged(SM_CHANGE_TYPE_TIMEZONE)
            || !schedMeetingChangeset.getUpdatedTimeZone())
    {
        return nullptr;
    }

    MegaStringList* updatedTimezone = MegaStringList::createInstance();
    updatedTimezone->add(Base64::atob(schedMeetingChangeset.getUpdatedTimeZone()->oldValue).c_str());
    updatedTimezone->add(Base64::atob(schedMeetingChangeset.getUpdatedTimeZone()->newValue).c_str());
    return updatedTimezone;
}

MegaIntegerList* MegaUserAlertPrivate::getUpdatedStartDate() const
{
    if (!hasSchedMeetingChanged(SM_CHANGE_TYPE_STARTDATE)
            || !schedMeetingChangeset.getUpdatedStartDateTime())
    {
        return nullptr;
    }

    MegaIntegerList* updatedStartDateTime = MegaIntegerList::createInstance();
    updatedStartDateTime->add(schedMeetingChangeset.getUpdatedStartDateTime()->oldValue);
    updatedStartDateTime->add(schedMeetingChangeset.getUpdatedStartDateTime()->newValue);
    return updatedStartDateTime;
}

MegaIntegerList* MegaUserAlertPrivate::getUpdatedEndDate() const
{
    if (!hasSchedMeetingChanged(SM_CHANGE_TYPE_ENDDATE)
            || !schedMeetingChangeset.getUpdatedEndDateTime())
    {
        return nullptr;
    }

    MegaIntegerList* updatedEndDateTime = MegaIntegerList::createInstance();
    updatedEndDateTime->add(schedMeetingChangeset.getUpdatedEndDateTime()->oldValue);
    updatedEndDateTime->add(schedMeetingChangeset.getUpdatedEndDateTime()->newValue);
    return updatedEndDateTime;
}
#endif

bool MegaUserAlertPrivate::isOwnChange() const
{
    return tag != 0;
}

bool mega::MegaUserAlertPrivate::isRemoved() const
{
    return removed;
}

MegaNode *MegaNodePrivate::fromNode(Node *node)
{
    if(!node) return NULL;
    return new MegaNodePrivate(node);
}

MegaSharePrivate::MegaSharePrivate(MegaShare *share) : MegaShare()
{
    this->nodehandle = share->getNodeHandle();
    this->user = MegaApi::strdup(share->getUser());
    this->access = share->getAccess();
    this->ts = share->getTimestamp();
    this->pending = share->isPending();
    this->mVerified = share->isVerified();
}

MegaShare *MegaSharePrivate::copy()
{
    return new MegaSharePrivate(this);
}

MegaSharePrivate::MegaSharePrivate(const impl::ShareData& data)
{
    // Convenience
    const Share* share = data.getShare();

    this->nodehandle = data.getNodeHandle();
    this->user = share->user ? MegaApi::strdup(share->user->email.c_str()) : NULL;
    if ((!user || !*user) && share->pcr)
    {
        delete [] user;
        user = MegaApi::strdup(share->pcr->isoutgoing ? share->pcr->targetemail.c_str() : share->pcr->originatoremail.c_str());
    }
    this->access = share->access;
    this->ts = share->ts;
    this->pending = share->pcr != nullptr;
    this->mVerified = data.isVerified();
}

MegaShare* MegaSharePrivate::fromShare(const impl::ShareData& data)
{
    return new MegaSharePrivate(data);
}

MegaSharePrivate::~MegaSharePrivate()
{
    delete[] user;
}

const char *MegaSharePrivate::getUser()
{
    return user;
}

uint64_t MegaSharePrivate::getNodeHandle()
{
    return nodehandle;
}

int MegaSharePrivate::getAccess()
{
    return access;
}

int64_t MegaSharePrivate::getTimestamp()
{
    return ts;
}

bool MegaSharePrivate::isPending()
{
    return pending;
}

bool MegaSharePrivate::isVerified()
{
    return mVerified;
}


MegaTransferPrivate::MegaTransferPrivate(int type, MegaTransferListener *listener)
    : mCollisionCheck(CollisionChecker::Option::Fingerprint)
    , mCollisionResolution(CollisionResolution::RenameNewWithN)
    , mCollisionCheckResult(CollisionChecker::Result::NotYet)
    , mFsType(FileSystemType::FS_UNKNOWN)
{
    this->type = type;
    this->tag = -1;
    this->path = NULL;
    this->nodeHandle = UNDEF;
    this->parentHandle = UNDEF;
    this->startPos = -1;
    this->endPos = -1;
    this->parentPath = NULL;
    this->listener = listener;
    this->retry = 0;
    this->maxRetries = 7;
    this->time = -1;
    this->startTime = 0;
    this->transferredBytes = 0;
    this->totalBytes = 0;
    this->fileName = NULL;
    this->transfer = NULL;
    this->speed = 0;
    this->deltaSize = 0;
    this->updateTime = 0;
    this->publicNode = NULL;
    this->lastBytes = NULL;
    this->syncTransfer = false;
    this->streamingTransfer = false;
    this->temporarySourceFile = false;
    this->startFirst = false;
    this->backupTransfer = false;
    this->foreignOverquota = false;
    this->folderTransferTag = 0;
    this->appData = NULL;
    this->state = STATE_NONE;
    this->priority = 0;
    this->meanSpeed = 0;
    this->notificationNumber = 0;
    this->mStage = MegaTransfer::STAGE_NONE;
}

MegaTransferPrivate::MegaTransferPrivate(const MegaTransferPrivate *transfer)
{
    path = NULL;
    parentPath = NULL;
    fileName = NULL;
    publicNode = NULL;
    lastBytes = NULL;
    appData = NULL;

    this->listener = transfer->getListener();
    this->transfer = transfer->getTransfer();
    this->type = transfer->getType();
    this->dbid = transfer->getUniqueId();
    this->setState(transfer->getState());
    this->setPriority(transfer->getPriority());
    this->setTag(transfer->getTag());
    this->updateLocalPathInternal(transfer->getLocalPath());
    this->setNodeHandle(transfer->getNodeHandle());
    this->setParentHandle(transfer->getParentHandle());
    this->setStartPos(transfer->getStartPos());
    this->setEndPos(transfer->getEndPos());
    this->setParentPath(transfer->getParentPath());
    this->setNumRetry(transfer->getNumRetry());
    this->setMaxRetries(transfer->getMaxRetries());
    this->setTime(transfer->getTime());
    this->startTime = transfer->getStartTime();
    this->setTransferredBytes(transfer->getTransferredBytes());
    this->setTotalBytes(transfer->getTotalBytes());
    this->setFileName(transfer->getFileName());
    this->setSpeed(transfer->getSpeed());
    this->setMeanSpeed(transfer->getMeanSpeed());
    this->setDeltaSize(transfer->getDeltaSize());
    this->setUpdateTime(transfer->getUpdateTime());
    this->setPublicNode(transfer->getPublicNode());
    this->setTransfer(transfer->getTransfer());
    this->setSyncTransfer(transfer->isSyncTransfer());
    this->setStreamingTransfer(transfer->isStreamingTransfer());
    this->setSourceFileTemporary(transfer->isSourceFileTemporary());
    this->setStartFirst(transfer->shouldStartFirst());
    this->setBackupTransfer(transfer->isBackupTransfer());
    this->setForeignOverquota(transfer->isForeignOverquota());
    this->setForceNewUpload(transfer->isForceNewUpload());
    this->setLastError(transfer->lastError.get());
    this->setFolderTransferTag(transfer->getFolderTransferTag());
    this->setAppData(transfer->getAppData());
    this->setNotificationNumber(transfer->getNotificationNumber());
    mCancelToken = transfer->mCancelToken;
    this->setStage(transfer->getStage());
    this->setCollisionCheck(transfer->getCollisionCheck());
    this->setCollisionResolution(transfer->getCollisionResolution());
    this->setCollisionCheckResult(transfer->getCollisionCheckResult());
    this->setFileSystemType(transfer->getFileSystemType());
}

MegaTransfer* MegaTransferPrivate::copy()
{
    return new MegaTransferPrivate(this);
}

void MegaTransferPrivate::setTransfer(Transfer* newTransfer)
{
    transfer = newTransfer;
}

Transfer* MegaTransferPrivate::getTransfer() const
{
    return transfer;
}

uint32_t MegaTransferPrivate::getUniqueId() const
{
    return dbid;
}

int MegaTransferPrivate::getTag() const
{
    return tag;
}

long long MegaTransferPrivate::getSpeed() const
{
    return speed;
}

long long MegaTransferPrivate::getMeanSpeed() const
{
    return meanSpeed;
}

long long MegaTransferPrivate::getDeltaSize() const
{
    return deltaSize;
}

int64_t MegaTransferPrivate::getUpdateTime() const
{
    return updateTime;
}

MegaNode *MegaTransferPrivate::getPublicNode() const
{
    return publicNode;
}

MegaNode *MegaTransferPrivate::getPublicMegaNode() const
{
    if(publicNode)
    {
        return publicNode->copy();
    }

    return NULL;
}

bool MegaTransferPrivate::isSyncTransfer() const
{
    return syncTransfer;
}

bool MegaTransferPrivate::isStreamingTransfer() const
{
    return streamingTransfer;
}

bool MegaTransferPrivate::isFinished() const
{
    return state == STATE_COMPLETED || state == STATE_CANCELLED || state == STATE_FAILED;
}

bool MegaTransferPrivate::isBackupTransfer() const
{
    return backupTransfer;
}

bool MegaTransferPrivate::isForeignOverquota() const
{
    return foreignOverquota;
}

bool MegaTransferPrivate::isForceNewUpload() const
{
    return forceNewUpload;
}

bool MegaTransferPrivate::isSourceFileTemporary() const
{
    return temporarySourceFile;
}

bool MegaTransferPrivate::shouldStartFirst() const
{
    return startFirst;
}

int MegaTransferPrivate::getType() const
{
    return type;
}

int64_t MegaTransferPrivate::getStartTime() const
{
    return startTime;
}

long long MegaTransferPrivate::getTransferredBytes() const
{
    return transferredBytes;
}

long long MegaTransferPrivate::getTotalBytes() const
{
    return totalBytes;
}

const char* MegaTransferPrivate::getPath() const
{
    return path;
}

const char* MegaTransferPrivate::getParentPath() const
{
    return parentPath;
}

uint64_t MegaTransferPrivate::getNodeHandle() const
{
    return nodeHandle;
}

uint64_t MegaTransferPrivate::getParentHandle() const
{
    return parentHandle;
}

long long MegaTransferPrivate::getStartPos() const
{
    return startPos;
}

long long MegaTransferPrivate::getEndPos() const
{
    return endPos;
}

int MegaTransferPrivate::getNumRetry() const
{
    return retry;
}

unsigned MegaTransferPrivate::getStage() const
{
    return mStage;
}

int MegaTransferPrivate::getMaxRetries() const
{
    return maxRetries;
}

int64_t MegaTransferPrivate::getTime() const
{
    return time;
}

const char* MegaTransferPrivate::getFileName() const
{
    return fileName;
}

char * MegaTransferPrivate::getLastBytes() const
{
    return lastBytes;
}

const MegaError *MegaTransferPrivate::getLastErrorExtended() const
{
    return lastError.get();
}

bool MegaTransferPrivate::isFolderTransfer() const
{
    return folderTransferTag < 0;
}

int MegaTransferPrivate::getFolderTransferTag() const
{
    return this->folderTransferTag;
}

void MegaTransferPrivate::setAppData(const char *data)
{
    if (this->appData)
    {
        delete [] this->appData;
    }
    this->appData = MegaApi::strdup(data);
}

const char *MegaTransferPrivate::getAppData() const
{
    return this->appData;
}

void MegaTransferPrivate::setState(int newState)
{
    state = newState;
}

int MegaTransferPrivate::getState() const
{
    return state;
}

void MegaTransferPrivate::setPriority(unsigned long long p)
{
    this->priority = p;
}

unsigned long long MegaTransferPrivate::getPriority() const
{
    return priority;
}

long long MegaTransferPrivate::getNotificationNumber() const
{
    return notificationNumber;
}

bool MegaTransferPrivate::getTargetOverride() const
{
    return mTargetOverride;
}

CollisionChecker::Option MegaTransferPrivate::getCollisionCheck() const
{
    return mCollisionCheck;
}

CollisionChecker::Result MegaTransferPrivate::getCollisionCheckResult() const
{
    return mCollisionCheckResult;
}

CollisionResolution MegaTransferPrivate::getCollisionResolution() const
{
    return mCollisionResolution;
}

MegaNode* MegaTransferPrivate::getNodeToUndelete() const
{
    return nodeToUndelete.get();
}

LocalPath MegaTransferPrivate::getLocalPath() const
{
    return mLocalPath;
}

std::optional<string> MegaTransferPrivate::getInboxTarget()
{
    constexpr size_t stringLength = 11;
    constexpr char charSeparator = '@';
    bool uploadToInbox =
        ISUNDEF(getParentHandle()) && getParentPath() &&
        (strchr(getParentPath(), charSeparator) || (strlen(getParentPath()) == stringLength));
    auto inboxTarget =
        uploadToInbox ? std::make_optional<std::string>(getParentPath()) : std::nullopt;
    return inboxTarget;
}

Pitag MegaTransferPrivate::getPitag() const
{
    return mPitag;
}

void MegaTransferPrivate::updateLocalPathInternal(const LocalPath& newPath)
{
    if (path)
        delete[] path;

    mLocalPath = newPath;
    path = MegaApi::strdup(mLocalPath.toPath(false).c_str());
}

bool MegaTransferPrivate::serialize(string *d) const
{
    d->append((const char*)&type, sizeof(type));
    d->append((const char*)&nodeHandle, sizeof(nodeHandle));
    d->append((const char*)&parentHandle, sizeof(parentHandle));

    unsigned short ll;
    ll = 0;
    d->append((char*)&ll, sizeof(ll));

    ll = (unsigned short)(parentPath ? strlen(parentPath) + 1 : 0);
    d->append((char*)&ll, sizeof(ll));
    d->append(parentPath, ll);

    ll = (unsigned short)(fileName ? strlen(fileName) + 1 : 0);
    d->append((char*)&ll, sizeof(ll));
    d->append(fileName, ll);

    d->append((const char*)&folderTransferTag, sizeof(folderTransferTag));
    bool hasLocalPath = true;
    d->append(reinterpret_cast<const char*>(&hasLocalPath), sizeof(bool));
    d->append("\0\0\0\0\0", 6);

    ll = (unsigned short)(appData ? strlen(appData) + 1 : 0);
    if (ll)
    {
        char hasAppData = 1;
        d->append(&hasAppData, 1);
        d->append((char*)&ll, sizeof(ll));
        d->append(appData, ll);
    }
    else
    {
        d->append("", 1);
    }

    auto localPathSerialized = mLocalPath.serialize();
    ll = static_cast<unsigned short>(localPathSerialized.size());
    d->append(reinterpret_cast<char*>(&ll), sizeof(ll));
    d->append(localPathSerialized);

    MegaNodePrivate *node = dynamic_cast<MegaNodePrivate *>(publicNode);
    bool isPublic = (node != NULL);
    d->append((const char*)&isPublic, sizeof(bool));
    if (isPublic)
    {
        node->serialize(d);
    }

    return true;
}

MegaTransferPrivate *MegaTransferPrivate::unserialize(string *d)
{
    const char* ptr = d->data();
    const char* end = ptr + d->size();

    if (ptr + sizeof(int) + sizeof(MegaHandle)
            + sizeof(MegaHandle) + sizeof(unsigned short) > end)
    {
        LOG_err << "MegaTransfer unserialization failed - data too short";
        return NULL;
    }

    int type = MemAccess::get<int>(ptr);
    ptr += sizeof(int);

    MegaTransferPrivate *transfer = new MegaTransferPrivate(type);
    transfer->nodeHandle = MemAccess::get<MegaHandle>(ptr);
    ptr += sizeof(MegaHandle);

    transfer->parentHandle = MemAccess::get<MegaHandle>(ptr);
    ptr += sizeof(MegaHandle);

    unsigned short pathlen = MemAccess::get<unsigned short>(ptr);
    ptr += sizeof(unsigned short);

    if (ptr + pathlen + sizeof(unsigned short) > end)
    {
        LOG_err << "MegaTransfer unserialization failed - path too long";
        delete transfer;
        return NULL;
    }

    std::string path;
    if (pathlen)
    {
        path.assign(ptr, pathlen - 1);
    }
    ptr += pathlen;

    unsigned short parentPathLen = MemAccess::get<unsigned short>(ptr);
    ptr += sizeof(unsigned short);

    if (ptr + parentPathLen + sizeof(unsigned short) > end)
    {
        LOG_err << "MegaTransfer unserialization failed - parentpath too long";
        delete transfer;
        return NULL;
    }

    std::string parentPath;
    if (parentPathLen)
    {
        parentPath.assign(ptr, parentPathLen - 1);
    }
    ptr += parentPathLen;

    unsigned short fileNameLen = MemAccess::get<unsigned short>(ptr);
    ptr += sizeof(unsigned short);

    if (ptr + fileNameLen + sizeof(int) + 7 + sizeof(char) > end)
    {
        LOG_err << "MegaTransfer unserialization failed - filename too long";
        delete transfer;
        return NULL;
    }

    std::string fileName;
    if (fileNameLen)
    {
        fileName.assign(ptr, fileNameLen - 1);
    }
    ptr += fileNameLen;

    transfer->folderTransferTag = MemAccess::get<int>(ptr);
    ptr += sizeof(int);

    bool hasLocalPath = MemAccess::get<bool>(ptr);
    ptr += sizeof(bool);

    if (memcmp(ptr, "\0\0\0\0\0", 6))
    {
        LOG_err << "MegaTransfer unserialization failed - invalid version";
        delete transfer;
        return NULL;
    }
    ptr += 6;

    char hasAppData = MemAccess::get<char>(ptr);
    ptr += sizeof(char);
    if (hasAppData > 1)
    {
        LOG_err << "MegaTransfer unserialization failed - invalid app data";
        delete transfer;
        return NULL;
    }

    if (hasAppData)
    {
        if (ptr + sizeof(unsigned short) > end)
        {
            LOG_err << "MegaTransfer unserialization failed - no app data header";
            delete transfer;
            return NULL;
        }

        unsigned short appDataLen = MemAccess::get<unsigned short>(ptr);
        ptr += sizeof(unsigned short);
        if (!appDataLen || (ptr + appDataLen > end))
        {
            LOG_err << "MegaTransfer unserialization failed - invalid appData";
            delete transfer;
            return NULL;
        }

        string data;
        data.assign(ptr, appDataLen - 1);
        transfer->setAppData(data.c_str());
        ptr += appDataLen;
    }

    std::optional<LocalPath> localPath;
    if (hasLocalPath)
    {
        if (ptr + sizeof(unsigned short) > end)
        {
            LOG_err << "MegaTransfer unserialization failed - LocaPath size";
            delete transfer;
            return NULL;
        }

        unsigned short localPathLen = MemAccess::get<unsigned short>(ptr);
        ptr += sizeof(unsigned short);

        if (ptr + localPathLen > end)
        {
            LOG_err << "MegaTransfer unserialization failed - LocaPath";
            delete transfer;
            return NULL;
        }

        std::string data;
        data.assign(ptr, localPathLen);
        localPath = LocalPath::unserialize(data);
        if (!localPath.has_value())
        {
            LOG_err << "MegaTransfer unserialization failed - LocaPath::unserialize";
            delete transfer;
            return NULL;
        }

        ptr += localPathLen;
    }

    if (ptr + sizeof(bool) > end)
    {
        LOG_err << "MegaTransfer unserialization failed - reading public node";
        delete transfer;
        return NULL;
    }

    bool isPublic = MemAccess::get<bool>(ptr);
    ptr += sizeof(bool);

    d->erase(0, static_cast<size_t>(ptr - d->data()));

    if (isPublic)
    {
        MegaNodePrivate *publicNode = MegaNodePrivate::unserialize(d);
        if (!publicNode)
        {
            LOG_err << "MegaTransfer unserialization failed - unable to unserialize MegaNode";
            delete transfer;
            return NULL;
        }

        transfer->setPublicNode(publicNode);
        delete publicNode;
    }

    if (localPath.has_value())
    {
        transfer->setLocalPath(localPath.value());
    }
    else if (path.length())
    {
        transfer->setPath(path.c_str());
    }

    if (parentPath.length())
    {
        transfer->setParentPath(parentPath.c_str());
    }

    if (fileName.length())
    {
        transfer->setFileName(fileName.c_str());
    }

    return transfer;
}

void MegaTransferPrivate::setTag(int newTag)
{
    tag = newTag;
}

void MegaTransferPrivate::setSpeed(long long newSpeed)
{
    speed = newSpeed;
}

void MegaTransferPrivate::setMeanSpeed(long long newMeanSpeed)
{
    meanSpeed = newMeanSpeed;
}

void MegaTransferPrivate::setDeltaSize(long long newDeltaSize)
{
    deltaSize = newDeltaSize;
}

void MegaTransferPrivate::setUpdateTime(int64_t newUpdateTime)
{
    updateTime = newUpdateTime;
}

void MegaTransferPrivate::setPublicNode(MegaNode* newPublicNode, bool copyChildren)
{
    if (publicNode)
    {
        delete publicNode;
    }

    if (!newPublicNode)
    {
        publicNode = nullptr;
    }
    else
    {
        MegaNodePrivate* nodePrivate = new MegaNodePrivate(newPublicNode);
        MegaNodeListPrivate* children =
            dynamic_cast<MegaNodeListPrivate*>(newPublicNode->getChildren());
        if (children && copyChildren)
        {
            nodePrivate->setChildren(new MegaNodeListPrivate(children, true));
        }
        publicNode = nodePrivate;
    }
}

void MegaTransferPrivate::setNodeToUndelete(MegaNode* toUndelete)
{
    nodeToUndelete.reset(toUndelete ? toUndelete->copy() : nullptr);
}

void MegaTransferPrivate::setSyncTransfer(bool isSyncTransfer)
{
    syncTransfer = isSyncTransfer;
}

void MegaTransferPrivate::setSourceFileTemporary(bool temporary)
{
    this->temporarySourceFile = temporary;
}

void MegaTransferPrivate::setStartFirst(bool beFirst)
{
    startFirst = beFirst;
}

void MegaTransferPrivate::setBackupTransfer(bool isBackupTransfer)
{
    backupTransfer = isBackupTransfer;
}

void MegaTransferPrivate::setForeignOverquota(bool isForeignOverquota)
{
    foreignOverquota = isForeignOverquota;
}

void MegaTransferPrivate::setForceNewUpload(bool isForceNewUpload)
{
    forceNewUpload = isForceNewUpload;
}

void MegaTransferPrivate::setStreamingTransfer(bool isStreamingTransfer)
{
    streamingTransfer = isStreamingTransfer;
}

void MegaTransferPrivate::setStartTime(int64_t newStartTime)
{
    if (!startTime)
    {
        startTime = newStartTime;
    }
}

void MegaTransferPrivate::setTransferredBytes(long long newByteCount)
{
    transferredBytes = newByteCount;
}

void MegaTransferPrivate::setTotalBytes(long long newByteCount)
{
    totalBytes = newByteCount;
}

void MegaTransferPrivate::setLastBytes(char* newLastBytes)
{
    lastBytes = newLastBytes;
}

void MegaTransferPrivate::setLastError(const MegaError *e)
{
   lastError.reset(e ? e->copy() : nullptr);
}

void MegaTransferPrivate::setFolderTransferTag(int newFolderTag)
{
    folderTransferTag = newFolderTag;
}

void MegaTransferPrivate::setNotificationNumber(long long number)
{
    notificationNumber = number;
}

void MegaTransferPrivate::setListener(MegaTransferListener* newTransferListener)
{
    listener = newTransferListener;
}

void MegaTransferPrivate::setTargetOverride(bool targetOverride)
{
    mTargetOverride = targetOverride;
}

void MegaTransferPrivate::setCancelToken(CancelToken cancelToken)
{
    mCancelToken = MegaCancelTokenPrivate(cancelToken);
}

void MegaTransferPrivate::setCollisionCheck(CollisionChecker::Option collisionCheck)
{
    mCollisionCheck = collisionCheck;
}

void MegaTransferPrivate::setCollisionCheck(int collisionCheck)
{
    if (collisionCheck >= static_cast<int>(CollisionChecker::Option::End) || collisionCheck < static_cast<int>(CollisionChecker::Option::Begin))
    {
        mCollisionCheck = CollisionChecker::Option::Fingerprint;
    }
    else
    {
        mCollisionCheck = static_cast<CollisionChecker::Option>(collisionCheck);
    }
}

void MegaTransferPrivate::setCollisionCheckResult(CollisionChecker::Result result)
{
    mCollisionCheckResult = result;
}

void MegaTransferPrivate::setCollisionResolution(CollisionResolution collisionResolution)
{
    mCollisionResolution = collisionResolution;
}

void MegaTransferPrivate::setCollisionResolution(int collisionResolution)
{
    if (collisionResolution >= static_cast<int>(CollisionResolution::End) || collisionResolution < static_cast<int>(CollisionResolution::Begin))
    {
        mCollisionResolution = CollisionResolution::RenameNewWithN;
    }
    else
    {
        mCollisionResolution = static_cast<CollisionResolution>(collisionResolution);
    }
}

void MegaTransferPrivate::startRecursiveOperation(shared_ptr<MegaRecursiveOperation> op, MegaNode* node)
{
    assert(op && !recursiveOperation);
    recursiveOperation = std::move(op);

    // for folder transfers, we must have a CancelToken even if the user did not supply one
    // so that we can cancel the remainder of the batch if the user cancels the folder transfer
    if (!mCancelToken.cancelFlag.exists())
    {
        mCancelToken.cancelFlag = CancelToken(false);
    }

    recursiveOperation->start(node);
}

void MegaTransferPrivate::stopRecursiveOperationThread()
{
    if (recursiveOperation) recursiveOperation->ensureThreadStopped();
}

long long MegaTransferPrivate::getPlaceInQueue() const
{
    return placeInQueue;
}

void MegaTransferPrivate::setPlaceInQueue(long long value)
{
    placeInQueue = value;
}

MegaCancelToken* MegaTransferPrivate::getCancelToken()
{
    return mCancelToken.existencePtr();
}

size_t MegaTransferPrivate::getTotalRecursiveOperation() const
{
    return recursiveOperation ? recursiveOperation->getTransfersTotalCount() : 0;
}

void MegaTransferPrivate::setPath(const char* newPath)
{
    if (path)
        delete[] path;

    mLocalPath = LocalPath::fromRelativePath(newPath);
    path = MegaApi::strdup(newPath);

    for (int i = int(strlen(newPath) - 1); i >= 0; i--)
    {
        if (newPath[i] == LocalPath::localPathSeparator_utf8)
        {
            setFileName(&(newPath[i + 1]));
            char* parentFolderPath = MegaApi::strdup(newPath);
            parentFolderPath[i + 1] = '\0';
            setParentPath(parentFolderPath);
            delete[] parentFolderPath;
            return;
        }
    }

    setFileName(newPath);
}

void MegaTransferPrivate::setLocalPath(const LocalPath& newPath)
{
    updateLocalPathInternal(newPath);
    LocalPath name = mLocalPath.leafName();
    setFileName(name.toPath(false).c_str());

    bool parentPathAvailable = true;
#ifdef __ANDROID__
    // On Android, parentPath() is invalid for a URI base path with no appended leaves
    if (mLocalPath.isURI() && mLocalPath.isRootPath())
    {
        parentPathAvailable = false;
    }
#endif
    if (parentPathAvailable)
    {
        LocalPath parent = mLocalPath.parentPath();
        if (!parent.empty())
        {
            setParentPath(parent.toPath(false).c_str());
        }
    }
}

void MegaTransferPrivate::setParentPath(const char* newParentPath)
{
    if (parentPath)
        delete[] parentPath;
    parentPath = MegaApi::strdup(newParentPath);
}

void MegaTransferPrivate::setFileName(const char* newFileName)
{
    if (fileName)
        delete[] fileName;
    fileName = MegaApi::strdup(newFileName);
}

void MegaTransferPrivate::setNodeHandle(MegaHandle newNodeHandle)
{
    nodeHandle = newNodeHandle;
}

void MegaTransferPrivate::setParentHandle(MegaHandle newParentHandle)
{
    parentHandle = newParentHandle;
}

void MegaTransferPrivate::setStartPos(long long newStartPos)
{
    startPos = newStartPos;
}

void MegaTransferPrivate::setEndPos(long long newEndPos)
{
    endPos = newEndPos;
}

void MegaTransferPrivate::setNumRetry(int newRetryCount)
{
    retry = newRetryCount;
}

void MegaTransferPrivate::setStage(unsigned stage)
{
    this->mStage = static_cast<uint8_t>(stage);
}

void MegaTransferPrivate::setMaxRetries(int newMaxRetryCount)
{
    maxRetries = newMaxRetryCount;
}

void MegaTransferPrivate::setTime(int64_t newTime)
{
    time = newTime;
}

const char * MegaTransferPrivate::getTransferString() const
{
    switch(type)
    {
    case TYPE_UPLOAD:
        return "UPLOAD";
    case TYPE_DOWNLOAD:
        return "DOWNLOAD";
    case TYPE_LOCAL_TCP_DOWNLOAD:
        return "LOCAL_HTTP_DOWNLOAD";
    }

    return "UNKNOWN";
}

MegaTransferListener* MegaTransferPrivate::getListener() const
{
    return listener;
}

MegaTransferPrivate::~MegaTransferPrivate()
{
    if (recursiveOperation && !recursiveOperation->allSubtransfersResolved())
    {
        // Folder transfers can only be resolved after all their sub-transfers
        // are resolved.  Eg, cancellation is via setting the cancelToken for all subTransfers
        // Therefore, logically we should never be deleting this object- assert to catch any errors
        assert(false);

        // from review: also log just in case
        LOG_warn << "~MegaTransferPrivate called before all sub-transfers were resolved";
    }
    delete[] path;
    delete[] parentPath;
    delete [] fileName;
    delete [] appData;
    delete publicNode;
}

const char * MegaTransferPrivate::toString() const
{
    return getTransferString();
}

MegaContactRequestPrivate::MegaContactRequestPrivate(PendingContactRequest *request)
{
    handle = request->id;
    sourceEmail = request->originatoremail.size() ? MegaApi::strdup(request->originatoremail.c_str()) : NULL;
    sourceMessage = request->msg.size() ? MegaApi::strdup(request->msg.c_str()) : NULL;
    targetEmail = request->targetemail.size() ? MegaApi::strdup(request->targetemail.c_str()) : NULL;
    creationTime = request->ts;
    modificationTime = request->uts;
    autoaccepted = request->autoaccepted;

    if(request->changed.accepted)
    {
        status = MegaContactRequest::STATUS_ACCEPTED;
    }
    else if(request->changed.deleted)
    {
        status = MegaContactRequest::STATUS_DELETED;
    }
    else if(request->changed.denied)
    {
        status = MegaContactRequest::STATUS_DENIED;
    }
    else if(request->changed.ignored)
    {
        status = MegaContactRequest::STATUS_IGNORED;
    }
    else if(request->changed.reminded)
    {
        status = MegaContactRequest::STATUS_REMINDED;
    }
    else
    {
        status = MegaContactRequest::STATUS_UNRESOLVED;
    }

    outgoing = request->isoutgoing;
}

MegaContactRequestPrivate::MegaContactRequestPrivate(const MegaContactRequest *request)
{
    handle = request->getHandle();
    sourceEmail = MegaApi::strdup(request->getSourceEmail());
    sourceMessage = MegaApi::strdup(request->getSourceMessage());
    targetEmail = MegaApi::strdup(request->getTargetEmail());
    creationTime = request->getCreationTime();
    modificationTime = request->getModificationTime();
    status = request->getStatus();
    outgoing = request->isOutgoing();
    autoaccepted = request->isAutoAccepted();
}

MegaContactRequestPrivate::~MegaContactRequestPrivate()
{
    delete [] sourceEmail;
    delete [] sourceMessage;
    delete [] targetEmail;
}

MegaContactRequest *MegaContactRequestPrivate::fromContactRequest(PendingContactRequest *request)
{
    return new MegaContactRequestPrivate(request);
}

MegaContactRequest *MegaContactRequestPrivate::copy() const
{
    return new MegaContactRequestPrivate(this);
}

MegaHandle MegaContactRequestPrivate::getHandle() const
{
    return handle;
}

char *MegaContactRequestPrivate::getSourceEmail() const
{
    return sourceEmail;
}

char *MegaContactRequestPrivate::getSourceMessage() const
{
    return sourceMessage;
}

char *MegaContactRequestPrivate::getTargetEmail() const
{
    return targetEmail;
}

int64_t MegaContactRequestPrivate::getCreationTime() const
{
    return creationTime;
}

int64_t MegaContactRequestPrivate::getModificationTime() const
{
    return modificationTime;
}

int MegaContactRequestPrivate::getStatus() const
{
    return status;
}

bool MegaContactRequestPrivate::isOutgoing() const
{
    return outgoing;
}

bool MegaContactRequestPrivate::isAutoAccepted() const
{
    return autoaccepted;
}

MegaAccountDetails *MegaAccountDetailsPrivate::fromAccountDetails(AccountDetails *details)
{
    return new MegaAccountDetailsPrivate(details);
}

MegaAccountDetailsPrivate::MegaAccountDetailsPrivate(AccountDetails *details)
{
    this->details = (*details);
}

MegaAccountDetailsPrivate::~MegaAccountDetailsPrivate()
{ }

MegaRequest *MegaRequestPrivate::copy()
{
    return new MegaRequestPrivate(this);
}

MegaRequestPrivate::MegaRequestPrivate(int type, MegaRequestListener *listener)
{
    this->type = type;
    this->tag = 0;
    this->transfer = 0;
    this->listener = listener;
    this->backupListener = NULL;
    this->nodeHandle = UNDEF;
    this->link = NULL;
    this->parentHandle = UNDEF;
    this->sessionKey = NULL;
    this->name = NULL;
    this->email = NULL;
    this->text = NULL;
    this->password = NULL;
    this->newPassword = NULL;
    this->privateKey = NULL;
    this->access = MegaShare::ACCESS_UNKNOWN;
    this->numRetry = 0;
    this->publicNode = NULL;
    this->numDetails = 0;
    this->file = NULL;
    this->attrType = 0;
    this->flag = false;
    this->totalBytes = -1;
    this->transferredBytes = 0;
    this->number = 0;
    this->timeZoneDetails = NULL;

    if (type == MegaRequest::TYPE_ACCOUNT_DETAILS || type == MegaRequest::TYPE_GET_RECOMMENDED_PRO_PLAN)
    {
        this->accountDetails = std::make_shared<AccountDetails>();
    }
    else
    {
        this->accountDetails.reset();
    }

    if (type == MegaRequest::TYPE_GET_ACHIEVEMENTS)
    {
        this->achievementsDetails = new AchievementsDetails();
    }
    else
    {
        this->achievementsDetails = NULL;
    }

    if ((type == MegaRequest::TYPE_GET_PRICING) || (type == MegaRequest::TYPE_GET_PAYMENT_ID) || type == MegaRequest::TYPE_UPGRADE_ACCOUNT || type == MegaRequest::TYPE_GET_RECOMMENDED_PRO_PLAN)
    {
        megaPricing = new MegaPricingPrivate();
        megaCurrency = new MegaCurrencyPrivate();
    }
    else
    {
        megaPricing = NULL;
        megaCurrency = nullptr;
    }

#ifdef ENABLE_CHAT
    if (type == MegaRequest::TYPE_CHAT_CREATE)
    {
        this->chatPeerList = new MegaTextChatPeerListPrivate();
    }
    else
    {
        this->chatPeerList = NULL;
    }

    if (type == MegaRequest::TYPE_CHAT_FETCH)
    {
        this->chatList = new MegaTextChatListPrivate();
    }
    else
    {
        this->chatList = NULL;
    }
#endif

    stringMap = NULL;
    mStringListMap = NULL;
    mStringTable = NULL;
    folderInfo = NULL;
    settings = NULL;
    backgroundMediaUpload = NULL;
}

MegaRequestPrivate::MegaRequestPrivate(MegaRequestPrivate *request)
{
    this->link = NULL;
    this->sessionKey = NULL;
    this->name = NULL;
    this->email = NULL;
    this->text = NULL;
    this->password = NULL;
    this->newPassword = NULL;
    this->privateKey = NULL;
    this->access = MegaShare::ACCESS_UNKNOWN;
    this->file = NULL;
    this->publicNode = NULL;
    this->type = request->getType();
    this->setTag(request->getTag());
    this->setNodeHandle(request->getNodeHandle());
    this->setLink(request->getLink());
    this->setParentHandle(request->getParentHandle());
    this->setSessionKey(request->getSessionKey());
    this->setName(request->getName());
    this->setEmail(request->getEmail());
    this->setPassword(request->getPassword());
    this->setNewPassword(request->getNewPassword());
    this->setPrivateKey(request->getPrivateKey());
    this->setAccess(request->getAccess());
    this->setNumRetry(request->getNumRetry());
    this->setNumDetails(request->getNumDetails());
    this->setFile(request->getFile());
    this->setParamType(request->getParamType());
    this->setText(request->getText());
    this->setNumber(request->getNumber());
    this->setPublicNode(request->getPublicNode());
    this->setFlag(request->getFlag());
    this->setTransferTag(request->getTransferTag());
    this->setTotalBytes(request->getTotalBytes());
    this->setTransferredBytes(request->getTransferredBytes());
    this->listener = request->getListener();

    this->backupListener = request->getBackupListener();
    this->megaPricing = (MegaPricingPrivate *)request->getPricing();
    this->megaCurrency = (MegaCurrencyPrivate *)request->getCurrency();

    this->accountDetails.reset();
    if(request->getAccountDetails())
    {
        this->accountDetails = std::make_shared<AccountDetails>();
        *(this->accountDetails) = *(request->getAccountDetails());
    }

    this->achievementsDetails = NULL;
    if(request->getAchievementsDetails())
    {
        this->achievementsDetails = new AchievementsDetails();
        *(this->achievementsDetails) = *(request->getAchievementsDetails());
    }

    this->timeZoneDetails = request->getMegaTimeZoneDetails() ? request->timeZoneDetails->copy() : NULL;

#ifdef ENABLE_CHAT
    this->chatPeerList = request->getMegaTextChatPeerList() ? request->chatPeerList->copy() : NULL;
    this->chatList = request->getMegaTextChatList() ? request->chatList->copy() : NULL;
    this->mScheduledMeetingList.reset(request->mScheduledMeetingList ? request->mScheduledMeetingList->copy() : nullptr);
#endif

    this->stringMap = request->getMegaStringMap() ? request->stringMap->copy() : NULL;
    this->mStringListMap = request->getMegaStringListMap() ? request->mStringListMap->copy() : NULL;
    this->mStringTable = request->getMegaStringTable() ? request->mStringTable->copy() : NULL;
    this->folderInfo = request->getMegaFolderInfo() ? request->folderInfo->copy() : NULL;
    this->settings = request->getMegaPushNotificationSettings() ? request->settings->copy() : NULL;
    this->backgroundMediaUpload = NULL;
    this->mBannerList.reset(request->mBannerList ? request->mBannerList->copy() : nullptr);
    this->mHandleList.reset(request->mHandleList ? request->mHandleList->copy() : nullptr);
    this->mRecentActions.reset(request->mRecentActions ? request->mRecentActions->copy() : nullptr);
    this->mMegaBackupInfoList.reset(request->mMegaBackupInfoList ? request->mMegaBackupInfoList->copy() : nullptr);
    this->mMegaSet.reset(request->mMegaSet ? request->mMegaSet->copy() : nullptr);
    this->mMegaSetElementList.reset(request->mMegaSetElementList ? request->mMegaSetElementList->copy() : nullptr);
    this->mMegaIntegerList.reset(request->mMegaIntegerList ? request->mMegaIntegerList->copy() : nullptr);
#ifdef ENABLE_SYNC
    if (request->mSyncStallList)
        mSyncStallList.reset(request->mSyncStallList->copy());

    if (request->mSyncStallMap)
    {
        mSyncStallMap.reset(request->mSyncStallMap->copy());
    }
#endif // ENABLE_SYNC
    this->mStringList.reset(request->mStringList ? request->mStringList->copy() : nullptr);
    this->mMegaVpnRegions.reset(request->mMegaVpnRegions ? request->mMegaVpnRegions->copy() :
                                                           nullptr);
    this->mMegaVpnCredentials.reset(request->mMegaVpnCredentials ? request->mMegaVpnCredentials->copy() : nullptr);
    mNetworkConnectivityTestResults.reset(request->mNetworkConnectivityTestResults ?
                                              request->mNetworkConnectivityTestResults->copy() :
                                              nullptr);
    this->mMegaNotifications.reset(request->mMegaNotifications ? request->mMegaNotifications->copy() : nullptr);
    this->mMegaNodeTree.reset(request->mMegaNodeTree ? request->mMegaNodeTree->copy() : nullptr);
    this->mStringIntegerMap.reset(request->mStringIntegerMap ? request->mStringIntegerMap->copy() :
                                                               nullptr);
    this->mMegaDiscountCodeList.reset(
        request->mMegaDiscountCodeList ? request->mMegaDiscountCodeList->copy() : nullptr);
    this->mMegaDiscountCodeInfo.reset(
        request->mMegaDiscountCodeInfo ? request->mMegaDiscountCodeInfo->copy() : nullptr);
    this->mMegaFileServiceReclaimOptions.reset(request->mMegaFileServiceReclaimOptions ?
                                                   request->mMegaFileServiceReclaimOptions->copy() :
                                                   nullptr);
}

std::shared_ptr<AccountDetails> MegaRequestPrivate::getAccountDetails() const
{
    return accountDetails;
}

MegaAchievementsDetails *MegaRequestPrivate::getMegaAchievementsDetails() const
{
    if (achievementsDetails)
    {
        return MegaAchievementsDetailsPrivate::fromAchievementsDetails(achievementsDetails);
    }
    return NULL;
}

AchievementsDetails *MegaRequestPrivate::getAchievementsDetails() const
{
    return achievementsDetails;
}

MegaTimeZoneDetails *MegaRequestPrivate::getMegaTimeZoneDetails() const
{
    return timeZoneDetails;
}

MegaStringList *MegaRequestPrivate::getMegaStringList() const
{
    return mStringList.get();
}

MegaStringIntegerMap* MegaRequestPrivate::getMegaStringIntegerMap() const
{
    return mStringIntegerMap.get();
}

MegaHandleList* MegaRequestPrivate::getMegaHandleList() const
{
    return mHandleList.get();
}

#ifdef ENABLE_SYNC

MegaSyncStallList* MegaRequestPrivate::getMegaSyncStallList() const
{
    return mSyncStallList.get();
}

MegaSyncStallMap* MegaRequestPrivate::getMegaSyncStallMap() const
{
    return mSyncStallMap.get();
}

void MegaRequestPrivate::setMegaSyncStallList(unique_ptr<MegaSyncStallList>&& sl)
{
    mSyncStallList = std::move(sl);
}

void MegaRequestPrivate::setMegaSyncStallMap(std::unique_ptr<MegaSyncStallMap>&& sm)
{
    mSyncStallMap = std::move(sm);
}
#endif // ENABLE_SYNC

#ifdef ENABLE_CHAT
MegaTextChatPeerList *MegaRequestPrivate::getMegaTextChatPeerList() const
{
    return chatPeerList;
}

void MegaRequestPrivate::setMegaTextChatPeerList(MegaTextChatPeerList *chatPeers)
{
    if (this->chatPeerList)
    {
        delete this->chatPeerList;
    }
    this->chatPeerList = chatPeers ? chatPeers->copy() : nullptr;
}

MegaTextChatList *MegaRequestPrivate::getMegaTextChatList() const
{
    return chatList;
}

void MegaRequestPrivate::setMegaTextChatList(MegaTextChatList* newChatList)
{
    if (chatList)
        delete chatList;

    chatList = newChatList->copy();
}

MegaScheduledMeetingList* MegaRequestPrivate::getMegaScheduledMeetingList() const
{
    return mScheduledMeetingList.get();
}

void MegaRequestPrivate::setMegaScheduledMeetingList(const MegaScheduledMeetingList* schedMeetingList)
{
    mScheduledMeetingList.reset();

    if (schedMeetingList)
    {
       mScheduledMeetingList = unique_ptr<MegaScheduledMeetingList>(schedMeetingList->copy());
    }
}
#endif

MegaStringMap *MegaRequestPrivate::getMegaStringMap() const
{
    return stringMap;
}

void MegaRequestPrivate::setMegaStringMap(const MegaStringMap* newStringMap)
{
    if (stringMap)
    {
        delete stringMap;
    }

    stringMap = newStringMap ? newStringMap->copy() : nullptr;
}

void MegaRequestPrivate::setMegaStringMap(const map<string, string>& newValues)
{
    delete stringMap;
    stringMap = new MegaStringMapPrivate(&newValues);
}

MegaStringListMap *MegaRequestPrivate::getMegaStringListMap() const
{
    return mStringListMap;
}

void MegaRequestPrivate::setMegaStringListMap(const MegaStringListMap* stringListMap)
{
    if (mStringListMap)
    {
        delete mStringListMap;
    }
    mStringListMap = stringListMap ? stringListMap->copy() : nullptr;
}

MegaStringTable *MegaRequestPrivate::getMegaStringTable() const
{
    return mStringTable;
}

void MegaRequestPrivate::setMegaStringTable(const MegaStringTable* stringTable)
{
    if (mStringTable)
    {
        delete mStringTable;
    }
    mStringTable = stringTable ? stringTable->copy() : nullptr;
}

MegaFolderInfo *MegaRequestPrivate::getMegaFolderInfo() const
{
    return folderInfo;
}

void MegaRequestPrivate::setMegaFolderInfo(const MegaFolderInfo* newFolderInfo)
{
    if (folderInfo)
    {
        delete folderInfo;
    }

    folderInfo = newFolderInfo ? newFolderInfo->copy() : nullptr;
}

const MegaPushNotificationSettings *MegaRequestPrivate::getMegaPushNotificationSettings() const
{
    return settings;
}

void MegaRequestPrivate::setMegaPushNotificationSettings(
    const MegaPushNotificationSettings* newSettings)
{
    if (settings)
    {
        delete settings;
    }

    settings = newSettings ? newSettings->copy() : nullptr;
}

MegaBackgroundMediaUpload *MegaRequestPrivate::getMegaBackgroundMediaUploadPtr() const
{
    // non-owned pointer
    return backgroundMediaUpload;
}

void MegaRequestPrivate::setMegaBackgroundMediaUploadPtr(MegaBackgroundMediaUpload *p)
{
    // non-owned pointer
    backgroundMediaUpload = p;
}

void MegaRequestPrivate::setMegaStringList(const MegaStringList* stringList)
{
    mStringList.reset();

    if (stringList)
    {
       mStringList = unique_ptr<MegaStringList>(stringList->copy());
    }
}

void MegaRequestPrivate::setMegaStringIntegerMap(const MegaStringIntegerMap* stringIntegerMap)
{
    mStringIntegerMap.reset();

    if (stringIntegerMap)
    {
        mStringIntegerMap = std::unique_ptr<MegaStringIntegerMap>(stringIntegerMap->copy());
    }
}

void MegaRequestPrivate::setMegaHandleList(const vector<handle> &handles)
{
    mHandleList.reset(new MegaHandleListPrivate(handles));
}

void MegaRequestPrivate::setMegaHandleList(const MegaHandleList* handles)
{
    mHandleList.reset(handles ? handles->copy() : nullptr);
}

MegaScheduledCopyListener *MegaRequestPrivate::getBackupListener() const
{
    return backupListener;
}

void MegaRequestPrivate::setBackupListener(MegaScheduledCopyListener *value)
{
    backupListener = value;
}

MegaAccountDetails *MegaRequestPrivate::getMegaAccountDetails() const
{
    if(accountDetails)
    {
        return MegaAccountDetailsPrivate::fromAccountDetails(accountDetails.get());
    }
    return NULL;
}

MegaRequestPrivate::~MegaRequestPrivate()
{
    delete [] link;
    delete [] name;
    delete [] email;
    delete [] password;
    delete [] newPassword;
    delete [] privateKey;
    delete [] sessionKey;
    delete publicNode;
    delete [] file;
    delete megaPricing;
    delete megaCurrency;
    delete achievementsDetails;
    delete [] text;
    delete stringMap;
    delete mStringListMap;
    delete mStringTable;
    delete folderInfo;
    delete timeZoneDetails;
    delete settings;

#ifdef ENABLE_CHAT
    delete chatPeerList;
    delete chatList;
#endif
}

int MegaRequestPrivate::getType() const
{
    return type;
}

uint64_t MegaRequestPrivate::getNodeHandle() const
{
    return nodeHandle;
}

const char* MegaRequestPrivate::getLink() const
{
    return link;
}

uint64_t MegaRequestPrivate::getParentHandle() const
{
    return parentHandle;
}

const char* MegaRequestPrivate::getSessionKey() const
{
    return sessionKey;
}

const char* MegaRequestPrivate::getName() const
{
    return name;
}

const char* MegaRequestPrivate::getEmail() const
{
    return email;
}

const char* MegaRequestPrivate::getPassword() const
{
    return password;
}

const char* MegaRequestPrivate::getNewPassword() const
{
    return newPassword;
}

const char* MegaRequestPrivate::getPrivateKey() const
{
    return privateKey;
}

int MegaRequestPrivate::getAccess() const
{
    return access;
}

const char* MegaRequestPrivate::getFile() const
{
    return file;
}

int MegaRequestPrivate::getParamType() const
{
    return attrType;
}

const char *MegaRequestPrivate::getText() const
{
    return text;
}

long long MegaRequestPrivate::getNumber() const
{
    return number;
}

bool MegaRequestPrivate::getFlag() const
{
    return flag;
}

long long MegaRequestPrivate::getTransferredBytes() const
{
    return transferredBytes;
}

long long MegaRequestPrivate::getTotalBytes() const
{
    return totalBytes;
}

int MegaRequestPrivate::getNumRetry() const
{
    return numRetry;
}

int MegaRequestPrivate::getNumDetails() const
{
    return numDetails;
}

int MegaRequestPrivate::getTag() const
{
    return tag;
}

MegaPricing *MegaRequestPrivate::getPricing() const
{
    return megaPricing ? megaPricing->copy() : NULL;
}

MegaCurrency *MegaRequestPrivate::getCurrency() const
{
    return megaCurrency ? megaCurrency->copy() : nullptr;
}

void MegaRequestPrivate::setNumDetails(int count)
{
    numDetails = count;
}

MegaNode *MegaRequestPrivate::getPublicNode() const
{
    return publicNode;
}

MegaNode *MegaRequestPrivate::getPublicMegaNode() const
{
    if(publicNode)
    {
        return publicNode->copy();
    }

    return NULL;
}

void MegaRequestPrivate::setNodeHandle(MegaHandle newNodeHandle)
{
    nodeHandle = newNodeHandle;
}

void MegaRequestPrivate::setParentHandle(MegaHandle newParentHandle)
{
    parentHandle = newParentHandle;
}

void MegaRequestPrivate::setSessionKey(const char* newSessionKey)
{
    if (sessionKey)
        delete[] sessionKey;
    sessionKey = MegaApi::strdup(newSessionKey);
}

void MegaRequestPrivate::setNumRetry(int count)
{
    numRetry = count;
}

void MegaRequestPrivate::setLink(const char* newLink)
{
    if (link)
        delete[] link;

    link = MegaApi::strdup(newLink);
}

void MegaRequestPrivate::setName(const char* newName)
{
    if (name)
        delete[] name;

    name = MegaApi::strdup(newName);
}

void MegaRequestPrivate::setEmail(const char* newEmail)
{
    if (email)
        delete[] email;

    email = MegaApi::strdup(newEmail);
}

void MegaRequestPrivate::setPassword(const char* pass)
{
    if (password)
        delete[] password;

    password = MegaApi::strdup(pass);
}

void MegaRequestPrivate::setNewPassword(const char* pass)
{
    if (newPassword)
        delete[] newPassword;

    newPassword = MegaApi::strdup(pass);
}

void MegaRequestPrivate::setPrivateKey(const char* newPrivateKey)
{
    if (privateKey)
        delete[] privateKey;

    privateKey = MegaApi::strdup(newPrivateKey);
}

void MegaRequestPrivate::setAccess(int newAccess)
{
    access = newAccess;
}

void MegaRequestPrivate::setFile(const char* newFile)
{
    if (file)
        delete[] file;

    file = MegaApi::strdup(newFile);
}

void MegaRequestPrivate::setParamType(int newType)
{
    attrType = newType;
}

void MegaRequestPrivate::setText(const char* newText)
{
    if (text)
        delete[] text;
    text = MegaApi::strdup(newText);
}

void MegaRequestPrivate::setNumber(long long newNumber)
{
    number = newNumber;
}

void MegaRequestPrivate::setFlag(bool newFlag)
{
    flag = newFlag;
}

void MegaRequestPrivate::setTransferTag(int newTag)
{
    transfer = newTag;
}

void MegaRequestPrivate::setListener(MegaRequestListener* newListener)
{
    listener = newListener;
}

void MegaRequestPrivate::setTotalBytes(long long byteCount)
{
    totalBytes = byteCount;
}

void MegaRequestPrivate::setTransferredBytes(long long byteCount)
{
    transferredBytes = byteCount;
}

void MegaRequestPrivate::setTag(int newTag)
{
    tag = newTag;
}

void MegaRequestPrivate::addProduct(const Product& product)
{
    if (megaPricing)
    {
        megaPricing->addProduct(product);
    }
}

void MegaRequestPrivate::setCurrency(std::unique_ptr<CurrencyData> currencyData)
{
    if (megaCurrency)
    {
        megaCurrency->setCurrency(std::move(currencyData));
    }
}

void MegaRequestPrivate::setProxy(Proxy* newProxy)
{
    proxy = newProxy;
}

Proxy *MegaRequestPrivate::getProxy()
{
    return proxy;
}

void MegaRequestPrivate::setTimeZoneDetails(MegaTimeZoneDetails* newDetails)
{
    if (timeZoneDetails)
    {
        delete timeZoneDetails;
    }
    timeZoneDetails = newDetails ? newDetails->copy() : nullptr;
}

void MegaRequestPrivate::setPublicNode(MegaNode* newPublicNode, bool copyChildren)
{
    if (publicNode)
    {
        delete publicNode;
    }

    if (!newPublicNode)
    {
        publicNode = nullptr;
    }
    else
    {
        MegaNodePrivate* nodePrivate = new MegaNodePrivate(newPublicNode);
        MegaNodeListPrivate* children =
            dynamic_cast<MegaNodeListPrivate*>(newPublicNode->getChildren());
        if (children && copyChildren)
        {
            nodePrivate->setChildren(new MegaNodeListPrivate(children, true));
        }
        publicNode = nodePrivate;
    }
}

MegaBannerList* MegaRequestPrivate::getMegaBannerList() const
{
    return mBannerList.get();
}

void MegaRequestPrivate::setBanners(vector<BannerDetails>&& banners)
{
    mBannerList = std::make_unique<MegaBannerListPrivate>();

    for (auto&& b : banners)
    {
        mBannerList->add(MegaBannerPrivate(std::move(b)));
    }
}

MegaRecentActionBucketList* MegaRequestPrivate::getRecentActions() const
{
    return mRecentActions.get();
}

void MegaRequestPrivate::setRecentActions(std::unique_ptr<MegaRecentActionBucketList> recentActionBucketList)
{
    mRecentActions.reset(recentActionBucketList.release());
}

MegaSet* MegaRequestPrivate::getMegaSet() const
{
    return mMegaSet.get();
}

void MegaRequestPrivate::setMegaSet(std::unique_ptr<MegaSet> s)
{
    mMegaSet.swap(s);
}

MegaSetElementList* MegaRequestPrivate::getMegaSetElementList() const
{
    return mMegaSetElementList.get();
}

void MegaRequestPrivate::setMegaSetElementList(std::unique_ptr<MegaSetElementList> els)
{
    mMegaSetElementList.swap(els);
}

const MegaIntegerList* MegaRequestPrivate::getMegaIntegerList() const
{
    return mMegaIntegerList.get();
}

void MegaRequestPrivate::setMegaIntegerList(std::unique_ptr<MegaIntegerList> ints)
{
    mMegaIntegerList.swap(ints);
}

MegaBackupInfoList* MegaRequestPrivate::getMegaBackupInfoList() const
{
    return mMegaBackupInfoList.get();
}

void MegaRequestPrivate::setMegaBackupInfoList(std::unique_ptr<MegaBackupInfoList> bkps)
{
    mMegaBackupInfoList.swap(bkps);
}

MegaVpnRegionList* MegaRequestPrivate::getMegaVpnRegionsDetailed() const
{
    return mMegaVpnRegions.get();
}

void MegaRequestPrivate::setMegaVpnRegionsDetailed(MegaVpnRegionList* vpnRegions)
{
    mMegaVpnRegions.reset(vpnRegions);
}

MegaVpnCredentials* MegaRequestPrivate::getMegaVpnCredentials() const
{
    return mMegaVpnCredentials.get();
}

void MegaRequestPrivate::setMegaVpnCredentials(MegaVpnCredentials* megaVpnCredentials)
{
    mMegaVpnCredentials.reset(megaVpnCredentials);
}

MegaNetworkConnectivityTestResults*
    MegaRequestPrivate::getMegaNetworkConnectivityTestResults() const
{
    return mNetworkConnectivityTestResults.get();
}

void MegaRequestPrivate::setMegaNetworkConnectivityTestResults(
    MegaNetworkConnectivityTestResults* networkConnectivityTestResults)
{
    mNetworkConnectivityTestResults.reset(networkConnectivityTestResults);
}

const char *MegaRequestPrivate::getRequestString() const
{
    switch(type)
    {
        case TYPE_LOGIN: return "LOGIN";
        case TYPE_CREATE_FOLDER: return "CREATE_FOLDER";
        case TYPE_MOVE: return "MOVE";
        case TYPE_COPY: return "COPY";
        case TYPE_RENAME: return "RENAME";
        case TYPE_REMOVE: return "REMOVE";
        case TYPE_SHARE: return "SHARE";
        case TYPE_IMPORT_LINK: return "IMPORT_LINK";
        case TYPE_EXPORT: return "EXPORT";
        case TYPE_FETCH_NODES: return "FETCH_NODES";
        case TYPE_ACCOUNT_DETAILS: return "ACCOUNT_DETAILS";
        case TYPE_CHANGE_PW: return "CHANGE_PW";
        case TYPE_UPLOAD: return "UPLOAD";
        case TYPE_LOGOUT: return "LOGOUT";
        case TYPE_GET_PUBLIC_NODE: return "GET_PUBLIC_NODE";
        case TYPE_GET_ATTR_FILE: return "GET_ATTR_FILE";
        case TYPE_SET_ATTR_FILE: return "SET_ATTR_FILE";
        case TYPE_GET_ATTR_USER: return "GET_ATTR_USER";
        case TYPE_SET_ATTR_USER: return "SET_ATTR_USER";
        case TYPE_RETRY_PENDING_CONNECTIONS: return "RETRY_PENDING_CONNECTIONS";
        case TYPE_REMOVE_CONTACT: return "REMOVE_CONTACT";
        case TYPE_CREATE_ACCOUNT: return "CREATE_ACCOUNT";
        case TYPE_CONFIRM_ACCOUNT: return "CONFIRM_ACCOUNT";
        case TYPE_QUERY_SIGNUP_LINK: return "QUERY_SIGNUP_LINK";
        case TYPE_ADD_SYNC: return "ADD_SYNC";
        //case TYPE_ENABLE_SYNC: return "ENABLE_SYNC";
        //case TYPE_DISABLE_SYNC: return "DISABLE_SYNC";
        case TYPE_COPY_SYNC_CONFIG: return "TYPE_COPY_SYNC_CONFIG";
        case TYPE_COPY_CACHED_STATUS: return "TYPE_COPY_CACHED_STATUS";
        case TYPE_IMPORT_SYNC_CONFIGS: return "TYPE_IMPORT_SYNC_CONFIGS";
        case TYPE_REMOVE_SYNC: return "REMOVE_SYNC";
        case TYPE_REMOVE_SYNCS: return "REMOVE_SYNCS";
        case TYPE_PAUSE_TRANSFERS: return "PAUSE_TRANSFERS";
        case TYPE_CANCEL_TRANSFER: return "CANCEL_TRANSFER";
        case TYPE_CANCEL_TRANSFERS: return "CANCEL_TRANSFERS";
        case TYPE_DELETE: return "DELETE";
        case TYPE_REPORT_EVENT: return "REPORT_EVENT";
        case TYPE_CANCEL_ATTR_FILE: return "CANCEL_ATTR_FILE";
        case TYPE_GET_PRICING: return "GET_PRICING";
        case TYPE_GET_PAYMENT_ID: return "GET_PAYMENT_ID";
        case TYPE_UPGRADE_ACCOUNT: return "UPGRADE_ACCOUNT";
        case TYPE_GET_USER_DATA: return "GET_USER_DATA";
        case TYPE_LOAD_BALANCING: return "LOAD_BALANCING";
        case TYPE_KILL_SESSION: return "KILL_SESSION";
        case TYPE_SUBMIT_PURCHASE_RECEIPT: return "SUBMIT_PURCHASE_RECEIPT";
        case TYPE_CREDIT_CARD_STORE: return "CREDIT_CARD_STORE";
        case TYPE_CREDIT_CARD_QUERY_SUBSCRIPTIONS: return "CREDIT_CARD_QUERY_SUBSCRIPTIONS";
        case TYPE_CREDIT_CARD_CANCEL_SUBSCRIPTIONS: return "CREDIT_CARD_CANCEL_SUBSCRIPTIONS";
        case TYPE_GET_SESSION_TRANSFER_URL: return "GET_SESSION_TRANSFER_URL";
        case TYPE_GET_PAYMENT_METHODS: return "GET_PAYMENT_METHODS";
        case TYPE_INVITE_CONTACT: return "INVITE_CONTACT";
        case TYPE_REPLY_CONTACT_REQUEST: return "REPLY_CONTACT_REQUEST";
        case TYPE_SUBMIT_FEEDBACK: return "SUBMIT_FEEDBACK";
        case TYPE_SEND_EVENT: return "SEND_EVENT";
        case TYPE_CLEAN_RUBBISH_BIN: return "CLEAN_RUBBISH_BIN";
        case TYPE_SET_ATTR_NODE: return "SET_ATTR_NODE";
        case TYPE_CHAT_CREATE: return "CHAT_CREATE";
        case TYPE_CHAT_FETCH: return "CHAT_FETCH";
        case TYPE_CHAT_INVITE: return "CHAT_INVITE";
        case TYPE_CHAT_REMOVE: return "CHAT_REMOVE";
        case TYPE_CHAT_URL: return "CHAT_URL";
        case TYPE_CHAT_GRANT_ACCESS: return "CHAT_GRANT_ACCESS";
        case TYPE_CHAT_REMOVE_ACCESS:
            return "CHAT_REMOVE_ACCESS";
        case TYPE_SET_PROXY: return "SET_PROXY";
        case TYPE_GET_RECOVERY_LINK: return "GET_RECOVERY_LINK";
        case TYPE_QUERY_RECOVERY_LINK: return "QUERY_RECOVERY_LINK";
        case TYPE_CONFIRM_RECOVERY_LINK: return "CONFIRM_RECOVERY_LINK";
        case TYPE_GET_CANCEL_LINK: return "GET_CANCEL_LINK";
        case TYPE_CONFIRM_CANCEL_LINK: return "CONFIRM_CANCEL_LINK";
        case TYPE_GET_CHANGE_EMAIL_LINK: return "GET_CHANGE_EMAIL_LINK";
        case TYPE_CONFIRM_CHANGE_EMAIL_LINK: return "CONFIRM_CHANGE_EMAIL_LINK";
        case TYPE_PAUSE_TRANSFER: return "PAUSE_TRANSFER";
        case TYPE_MOVE_TRANSFER: return "MOVE_TRANSFER";
        case TYPE_CHAT_SET_TITLE: return "CHAT_SET_TITLE";
        case TYPE_CHAT_UPDATE_PERMISSIONS: return "CHAT_UPDATE_PERMISSIONS";
        case TYPE_CHAT_TRUNCATE: return "CHAT_TRUNCATE";
        case TYPE_SET_MAX_CONNECTIONS: return "SET_MAX_CONNECTIONS";
        case TYPE_CHAT_PRESENCE_URL: return "CHAT_PRESENCE_URL";
        case TYPE_REGISTER_PUSH_NOTIFICATION: return "REGISTER_PUSH_NOTIFICATION";
        case TYPE_GET_USER_EMAIL: return "GET_USER_EMAIL";
        case TYPE_APP_VERSION: return "APP_VERSION";
        case TYPE_GET_LOCAL_SSL_CERT: return "GET_LOCAL_SSL_CERT";
        case TYPE_SEND_SIGNUP_LINK: return "SEND_SIGNUP_LINK";
        case TYPE_QUERY_DNS: return "QUERY_DNS";
        case TYPE_CHAT_STATS: return "CHAT_STATS";
        case TYPE_DOWNLOAD_FILE: return "DOWNLOAD_FILE";
        case TYPE_QUERY_TRANSFER_QUOTA: return "QUERY_TRANSFER_QUOTA";
        case TYPE_PASSWORD_LINK: return "PASSWORD_LINK";
        case TYPE_RESTORE: return "RESTORE";
        case TYPE_GET_ACHIEVEMENTS: return "GET_ACHIEVEMENTS";
        case TYPE_REMOVE_VERSIONS: return "REMOVE_VERSIONS";
        case TYPE_CHAT_ARCHIVE: return "CHAT_ARCHIVE";
        case TYPE_WHY_AM_I_BLOCKED: return "WHY_AM_I_BLOCKED";
        case TYPE_CONTACT_LINK_CREATE: return "CONTACT_LINK_CREATE";
        case TYPE_CONTACT_LINK_QUERY: return "CONTACT_LINK_QUERY";
        case TYPE_CONTACT_LINK_DELETE: return "CONTACT_LINK_DELETE";
        case TYPE_FOLDER_INFO: return "FOLDER_INFO";
        case TYPE_RICH_LINK: return "RICH_LINK";
        case TYPE_CHAT_LINK_HANDLE: return "CHAT_LINK_HANDLE";
        case TYPE_CHAT_LINK_URL: return "CHAT_LINK_URL";
        case TYPE_SET_PRIVATE_MODE: return "SET_PRIVATE_MODE";
        case TYPE_AUTOJOIN_PUBLIC_CHAT: return "AUTOJOIN_PUBLIC_CHAT";
        case TYPE_KEEP_ME_ALIVE: return "KEEP_ME_ALIVE";
        case TYPE_MULTI_FACTOR_AUTH_CHECK: return "MULTI_FACTOR_AUTH_CHECK";
        case TYPE_MULTI_FACTOR_AUTH_GET: return "MULTI_FACTOR_AUTH_GET";
        case TYPE_MULTI_FACTOR_AUTH_SET: return "MULTI_FACTOR_AUTH_SET";
        case TYPE_ADD_SCHEDULED_COPY: return "ADD_BACKUP"; // TODO: consider renaming this in the future.
        case TYPE_REMOVE_SCHEDULED_COPY: return "REMOVE_BACKUP"; // TODO: consider renaming this in the future..
        case TYPE_ABORT_CURRENT_SCHEDULED_COPY: return "ABORT_BACKUP"; // TODO: consider renaming this in the future.
        case TYPE_TIMER: return "SET_TIMER";
        case TYPE_GET_PSA: return "GET_PSA";
        case TYPE_FETCH_TIMEZONE: return "FETCH_TIMEZONE";
        case TYPE_USERALERT_ACKNOWLEDGE: return "USERALERT_ACKNOWLEDGE";
        case TYPE_CATCHUP: return "CATCHUP";
        case TYPE_PUBLIC_LINK_INFORMATION: return "PUBLIC_LINK_INFORMATION";
        case TYPE_GET_BACKGROUND_UPLOAD_URL: return "GET_BACKGROUND_UPLOAD_URL";
        case TYPE_COMPLETE_BACKGROUND_UPLOAD: return "COMPLETE_BACKGROUND_UPLOAD";
        case TYPE_GET_CLOUD_STORAGE_USED: return "GET_CLOUD_STORAGE_USED";
        case TYPE_SEND_SMS_VERIFICATIONCODE: return "SEND_SMS_VERIFICATIONCODE";
        case TYPE_CHECK_SMS_VERIFICATIONCODE: return "CHECK_SMS_VERIFICATIONCODE";
        case TYPE_GET_COUNTRY_CALLING_CODES: return "GET_COUNTRY_CALLING_CODES";
        case TYPE_VERIFY_CREDENTIALS: return "VERIFY_CREDENTIALS";
        case TYPE_GET_MISC_FLAGS: return "GET_MISC_FLAGS";
        case TYPE_RESEND_VERIFICATION_EMAIL: return "RESEND_VERIFICATION_EMAIL";
        case TYPE_SUPPORT_TICKET: return "SUPPORT_TICKET";
        case TYPE_SET_RETENTION_TIME: return "SET_RETENTION_TIME";
        case TYPE_RESET_SMS_VERIFIED_NUMBER: return "RESET_SMS_VERIFIED_NUMBER";
        case TYPE_SEND_DEV_COMMAND: return "SEND_DEV_COMMAND";
        case TYPE_GET_BANNERS: return "GET_BANNERS";
        case TYPE_DISMISS_BANNER: return "DISMISS_BANNER";
        case TYPE_BACKUP_PUT: return "BACKUP_PUT";
        case TYPE_BACKUP_REMOVE: return "BACKUP_REMOVE";
        case TYPE_BACKUP_PUT_HEART_BEAT: return "BACKUP_PUT_HEART_BEAT";
        case TYPE_FETCH_ADS: return "FETCH_ADS";
        case TYPE_QUERY_ADS: return "QUERY_ADS";
        case TYPE_GET_ATTR_NODE: return "GET_ATTR_NODE";
        case TYPE_START_CHAT_CALL: return "START_CHAT_CALL";
        case TYPE_JOIN_CHAT_CALL: return "JOIN_CHAT_CALL";
        case TYPE_END_CHAT_CALL: return "END_CHAT_CALL";
        case TYPE_LOAD_EXTERNAL_DRIVE_BACKUPS: return "LOAD_EXTERNAL_DRIVE_BACKUPS";
        case TYPE_CLOSE_EXTERNAL_DRIVE_BACKUPS: return "CLOSE_EXTERNAL_DRIVE_BACKUPS";
        case TYPE_GET_DOWNLOAD_URLS: return "GET_DOWNLOAD_URLS";
        case TYPE_GET_FA_UPLOAD_URL: return "GET_FA_UPLOAD_URL";
        case TYPE_EXECUTE_ON_THREAD: return "EXECUTE_ON_THREAD";
        case TYPE_SET_CHAT_OPTIONS: return "SET_CHAT_OPTIONS";
        case TYPE_GET_RECENT_ACTIONS: return "GET_RECENT_ACTIONS";
        case TYPE_GET_RECENT_ACTION_BY_ID:
            return "GET_RECENT_ACTION_BY_ID";
        case TYPE_CHECK_RECOVERY_KEY: return "CHECK_RECOVERY_KEY";
        case TYPE_SET_MY_BACKUPS: return "SET_MY_BACKUPS";
        case TYPE_EXPORT_SET: return "EXPORT_SET";
        case TYPE_PUT_SET: return "PUT_SET";
        case TYPE_REMOVE_SET: return "REMOVE_SET";
        case TYPE_FETCH_SET: return "FETCH_SET";
        case TYPE_PUT_SET_ELEMENTS: return "PUT_SET_ELEMENTS";
        case TYPE_PUT_SET_ELEMENT: return "PUT_SET_ELEMENT";
        case TYPE_REMOVE_SET_ELEMENT: return "REMOVE_SET_ELEMENT";
        case TYPE_REMOVE_SET_ELEMENTS: return "REMOVE_SET_ELEMENTS";
        case TYPE_REMOVE_OLD_BACKUP_NODES: return "REMOVE_OLD_BACKUP_NODES";
        case TYPE_SET_SYNC_RUNSTATE: return "SET_SYNC_RUNSTATE";
        case TYPE_ADD_UPDATE_SCHEDULED_MEETING: return "ADD_SCHEDULED_MEETING";
        case TYPE_DEL_SCHEDULED_MEETING: return "DEL_SCHEDULED_MEETING";
        case TYPE_FETCH_SCHEDULED_MEETING: return "FETCH_SCHEDULED_MEETING";
        case TYPE_FETCH_SCHEDULED_MEETING_OCCURRENCES: return "FETCH_SCHEDULED_MEETING_EVENTS";
        case TYPE_GET_EXPORTED_SET_ELEMENT: return "GET_EXPORTED_SET_ELEMENT";
        case TYPE_OPEN_SHARE_DIALOG: return "OPEN_SHARE_DIALOG";
        case TYPE_UPGRADE_SECURITY: return "UPGRADE_SECURITY";
        case TYPE_GET_RECOMMENDED_PRO_PLAN: return "GET_RECOMMENDED_PRO_PLAN";
        case TYPE_BACKUP_INFO: return "BACKUP_INFO";
        case TYPE_BACKUP_REMOVE_MD: return "BACKUP_REMOVE_MD";
        case TYPE_AB_TEST_ACTIVE: return "AB_TEST_ACTIVE";
        case TYPE_GET_VPN_REGIONS: return "GET_VPN_REGIONS";
        case TYPE_GET_VPN_CREDENTIALS: return "GET_VPN_CREDENTIALS";
        case TYPE_PUT_VPN_CREDENTIAL: return "PUT_VPN_CREDENTIAL";
        case TYPE_DEL_VPN_CREDENTIAL: return "DEL_VPN_CREDENTIAL";
        case TYPE_CHECK_VPN_CREDENTIAL: return "CHECK_VPN_CREDENTIAL";
        case TYPE_GET_SYNC_STALL_LIST: return "GET_SYNC_STALL_LIST";
        case TYPE_FETCH_CREDIT_CARD_INFO: return "FETCH_CREDIT_CARD_INFO";
        case TYPE_MOVE_TO_DEBRIS: return "MOVE_TO_DEBRIS";
        case TYPE_RING_INDIVIDUAL_IN_CALL: return "RING_INDIVIDUAL_IN_CALL";
        case TYPE_CREATE_NODE_TREE: return "CREATE_NODE_TREE";
        case TYPE_CREATE_PASSWORD_MANAGER_BASE: return "CREATE_PASSWORD_MANAGER_BASE";
        case TYPE_CREATE_PASSWORD_NODE: return "CREATE_PASSWORD_NODE";
        case TYPE_UPDATE_PASSWORD_NODE: return "UPDATE_PASSWORD_NODE";
        case TYPE_GET_NOTIFICATIONS: return "GET_NOTIFICATIONS";
        case TYPE_DEL_ATTR_USER:
            return "DEL_ATTR_USER";
        case TYPE_BACKUP_PAUSE_MD:
            return "BACKUP_PAUSE_MD";
        case TYPE_BACKUP_RESUME_MD:
            return "BACKUP_RESUME_MD";
        case TYPE_IMPORT_PASSWORDS_FROM_FILE:
            return "IMPORT_PASSWORDS_FROM_FILE";
        case TYPE_GET_SUBSCRIPTION_CANCELLATION_DETAILS:
            return "TYPE_GET_SUBSCRIPTION_CANCELLATION_DETAILS";
        case TYPE_GET_DISCOUNT_CODE_INFORMATION:
            return "TYPE_GET_DISCOUNT_CODE_INFORMATION";

        // FUSE requests.
        case TYPE_ADD_MOUNT:       return "TYPE_ADD_MOUNT";
        case TYPE_DISABLE_MOUNT:   return "TYPE_DISABLE_MOUNT";
        case TYPE_ENABLE_MOUNT:    return "TYPE_ENABLE_MOUNT";
        case TYPE_REMOVE_MOUNT:    return "TYPE_REMOVE_MOUNT";
        case TYPE_SET_MOUNT_FLAGS: return "TYPE_SET_MOUNT_FLAGS";
        case TYPE_GET_ACTIVE_SURVEY_TRIGGER_ACTIONS:
            return "TYPE_GET_ACTIVE_SURVEY_TRIGGER_ACTIONS";
        case TYPE_GET_SURVEY:
            return "TYPE_GET_SURVEY";
        case TYPE_ANSWER_SURVEY:
            return "TYPE_ANSWER_SURVEY";
        case TYPE_CHANGE_SYNC_ROOT:
            return "TYPE_CHANGE_SYNC_ROOT";
        case TYPE_GET_MY_IP:
            return "TYPE_GET_MY_IP";
        case TYPE_SET_SYNC_UPLOAD_THROTTLE_VALUES:
            return "TYPE_SET_SYNC_UPLOAD_THROTTLE_VALUES";
        case TYPE_GET_SYNC_UPLOAD_THROTTLE_VALUES:
            return "TYPE_GET_SYNC_UPLOAD_THROTTLE_VALUES";
        case TYPE_GET_SYNC_UPLOAD_THROTTLE_LIMITS:
            return "TYPE_GET_SYNC_UPLOAD_THROTTLE_LIMITS";
        case TYPE_RUN_NETWORK_CONNECTIVITY_TEST:
            return "TYPE_RUN_NETWORK_CONNECTIVITY_TEST";
        case TYPE_ADD_SYNC_PREVALIDATION:
            return "TYPE_ADD_SYNC_PREVALIDATION";
        case TYPE_GET_MAX_CONNECTIONS:
            return "GET_MAX_CONNECTIONS";
    }
    return "UNKNOWN";
}

MegaRequestListener *MegaRequestPrivate::getListener() const
{
    return listener;
}

int MegaRequestPrivate::getTransferTag() const
{
    return transfer;
}

const char *MegaRequestPrivate::toString() const
{
    return getRequestString();
}

const MegaNotificationList* MegaRequestPrivate::getMegaNotifications() const
{
    return mMegaNotifications.get();
}

void MegaRequestPrivate::setMegaNotifications(MegaNotificationList* megaNotifications)
{
    mMegaNotifications.reset(megaNotifications);
}

const MegaNodeTree* MegaRequestPrivate::getMegaNodeTree() const
{
    return mMegaNodeTree.get();
}

void MegaRequestPrivate::setMegaNodeTree(MegaNodeTree* megaNodeTree)
{
    mMegaNodeTree.reset(megaNodeTree);
}

const MegaCancelSubscriptionReasonList* MegaRequestPrivate::getMegaCancelSubscriptionReasons() const
{
    return mMegaCancelSubscriptionReasons.get();
}

void MegaRequestPrivate::setMegaCancelSubscriptionReasons(
    MegaCancelSubscriptionReasonList* cancelReasons)
{
    mMegaCancelSubscriptionReasons.reset(cancelReasons);
}

MegaDiscountCodeList* MegaRequestPrivate::getMegaDiscountCodeList() const
{
    return mMegaDiscountCodeList.get();
}

void MegaRequestPrivate::setMegaDiscountCodes(std::vector<DiscountCode>&& discountCodes)
{
    unique_ptr<MegaDiscountCodeListPrivate> tempList =
        std::make_unique<MegaDiscountCodeListPrivate>();
    for (auto& dc: discountCodes)
    {
        tempList->add(MegaDiscountCodePrivate(std::move(dc)));
    }
    mMegaDiscountCodeList = std::move(tempList);
}

const MegaDiscountCodeInfo* MegaRequestPrivate::getMegaDiscountCodeInfo() const
{
    return mMegaDiscountCodeInfo.get();
}

void MegaRequestPrivate::setMegaDiscountCodeInfo(
    std::unique_ptr<MegaDiscountCodeInfo> discountCodeInfo)
{
    mMegaDiscountCodeInfo = std::move(discountCodeInfo);
}

const MegaFileServiceReclaimOptions* MegaRequestPrivate::getMegaFileServiceReclaimOptions() const
{
    return mMegaFileServiceReclaimOptions.get();
}

void MegaRequestPrivate::setMegaFileServiceReclaimOptions(MegaFileServiceReclaimOptions* options)
{
    mMegaFileServiceReclaimOptions.reset(options);
}

bool MegaRequestPrivate::causesLocklessRequest(const int type)
{
    // List of request which create lockless commands.
    switch (type)
    {
        case TYPE_GET_DOWNLOAD_URLS:
        case TYPE_GET_PUBLIC_NODE:
        case TYPE_IMPORT_LINK:
            return true;
        default:
            return false;
    }
}

MegaBannerPrivate::MegaBannerPrivate(BannerDetails&& details):
    mDetails(std::move(details))
{
}

MegaBanner* MegaBannerPrivate::copy() const
{
    return new MegaBannerPrivate(*this);
}

int MegaBannerPrivate::getId() const
{
    return mDetails.id;
}

const char* MegaBannerPrivate::getTitle() const
{
    return mDetails.title.c_str();
}

const char* MegaBannerPrivate::getDescription() const
{
    return mDetails.description.c_str();
}

const char* MegaBannerPrivate::getImage() const
{
    return mDetails.image.c_str();
}

const char* MegaBannerPrivate::getUrl() const
{
    return mDetails.url.c_str();
}

const char* MegaBannerPrivate::getBackgroundImage() const
{
    return mDetails.backgroundImage.c_str();
}

const char* MegaBannerPrivate::getImageLocation() const
{
    return mDetails.imageLocation.c_str();
}

int MegaBannerPrivate::getVariant() const
{
    return mDetails.variant;
}

const char* MegaBannerPrivate::getButton() const
{
    return mDetails.button.c_str();
}

MegaBannerListPrivate* MegaBannerListPrivate::copy() const
{
    return new MegaBannerListPrivate(*this);
}

const MegaBanner* MegaBannerListPrivate::get(int i) const
{
    return (i >= 0 && static_cast<size_t>(i) < mVector.size()) ?
               &(mVector[static_cast<size_t>(i)]) :
               nullptr;
}

int MegaBannerListPrivate::size() const
{
    return int(mVector.size());
}

void MegaBannerListPrivate::add(MegaBannerPrivate&& banner)
{
    mVector.emplace_back(std::move(banner));
}

MegaStringMapPrivate::MegaStringMapPrivate()
{

}

MegaStringMapPrivate::MegaStringMapPrivate(const string_map *map, bool toBase64)
{
    strMap.insert(map->begin(),map->end());

    if (toBase64)
    {
        char* buf;
        string_map::iterator it;
        for (it = strMap.begin(); it != strMap.end(); it++)
        {
            buf = new char[it->second.length() * 4 / 3 + 4];
            Base64::btoa((const byte*)it->second.data(), it->second.length(), buf);

            it->second.assign(buf);

            delete [] buf;
        }
    }
}

MegaStringMapPrivate::~MegaStringMapPrivate()
{

}

MegaStringMap *MegaStringMapPrivate::copy() const
{
    return new MegaStringMapPrivate(this);
}

const char *MegaStringMapPrivate::get(const char *key) const
{
    string_map::const_iterator it = strMap.find(key);

    if (it == strMap.end())
    {
        return NULL;
    }

    return it->second.data();
}

MegaStringList *MegaStringMapPrivate::getKeys() const
{
    string_vector keys;
    for (auto& it : strMap)
    {
        keys.push_back(it.first);
    }

    return new MegaStringListPrivate(std::move(keys));
}

void MegaStringMapPrivate::set(const char *key, const char *value)
{
    strMap[key] = value;
}

int MegaStringMapPrivate::size() const
{
    return int(strMap.size());
}

const string_map *MegaStringMapPrivate::getMap() const
{
    return &strMap;
}

MegaStringMapPrivate::MegaStringMapPrivate(const MegaStringMapPrivate *megaStringMap)
{
    MegaStringList *keys = megaStringMap->getKeys();
    const char *key = NULL;
    const char *value = NULL;
    for (int i=0; i < keys->size(); i++)
    {
        key = keys->get(i);
        value = megaStringMap->get(key);

        strMap[key] = value;
    }

    delete keys;
}

MegaIntegerMapPrivate::MegaIntegerMapPrivate()
{
}

MegaIntegerMapPrivate::MegaIntegerMapPrivate(const MegaIntegerMapPrivate& megaIntegerMap)
    :mIntegerMap(megaIntegerMap.getMap() ? *megaIntegerMap.getMap() : integer_map())
{
}

MegaIntegerMapPrivate::MegaIntegerMapPrivate(const std::multimap<int8_t, int8_t>& bytesMap)
{
    for (const auto& element: bytesMap)
    {
        mIntegerMap.emplace(static_cast<int64_t>(element.first), static_cast<int64_t>(element.second));
    }
}

MegaIntegerMapPrivate::MegaIntegerMapPrivate(const std::multimap<int64_t, int64_t>& integerMap)
    :mIntegerMap(integerMap)
{
}

MegaIntegerMapPrivate::~MegaIntegerMapPrivate()
{
}


MegaSmallIntMap* MegaIntegerMapPrivate::toByteMap() const
{
    MegaSmallIntMap* byteMap = new MegaSmallIntMap();
    for (const auto& pair: mIntegerMap)
    {
        byteMap->emplace(static_cast<int8_t>(pair.first), static_cast<int8_t>(pair.second));
    }
    return byteMap;
}

MegaIntegerMap* MegaIntegerMapPrivate::copy() const
{
    return new MegaIntegerMapPrivate(*this);
}

MegaIntegerList* MegaIntegerMapPrivate::getKeys() const
{
    vector<int64_t> keys;
    for (auto& it : mIntegerMap)
    {
        keys.push_back(it.first);
    }

    return new MegaIntegerListPrivate(keys);
}

int64_t MegaIntegerMapPrivate::size() const
{
    return static_cast<int64_t>(mIntegerMap.size());
}

MegaIntegerList* MegaIntegerMapPrivate::get(int64_t key) const
{
    vector<int64_t> values;
    auto range = mIntegerMap.equal_range(key);
    for (auto i = range.first; i != range.second; ++i)
    {
        values.emplace_back(i->second);
    }
    return new MegaIntegerListPrivate(values);
}

void MegaIntegerMapPrivate::set(int64_t key, int64_t value)
{
    mIntegerMap.emplace(key, value);
}

const integer_map* MegaIntegerMapPrivate::getMap() const
{
    return &mIntegerMap;
}

MegaStringListPrivate* MegaStringIntegerMapPrivate::getKeys() const
{
    MegaStringListPrivate* keys = new MegaStringListPrivate();
    for (const auto& p : mStorage)
    {
        keys->add(p.first.c_str());
    }
    return keys;
}

MegaIntegerListPrivate* MegaStringIntegerMapPrivate::get(const char* key) const
{
    if (!key)
    {
        return nullptr;
    }

    auto it = mStorage.find(key);
    if (it == mStorage.end())
    {
        return nullptr;
    }

    MegaIntegerListPrivate* intList = new MegaIntegerListPrivate();
    intList->add(it->second);
    return intList;
}

void MegaStringIntegerMapPrivate::set(const char* key, int64_t value)
{
    assert(key);
    if (key)
    {
        mStorage[key] = value;
    }
}

void MegaStringIntegerMapPrivate::set(const string& key, int64_t value)
{
    mStorage[key] = value;
}

MegaStringListPrivate::MegaStringListPrivate(string_vector&& v)
    : mList(std::move(v))
{
}

MegaStringListPrivate::MegaStringListPrivate(const string_vector& v):
    mList(v)
{}

MegaStringList *MegaStringListPrivate::copy() const
{
    return new MegaStringListPrivate(*this);
}

const char *MegaStringListPrivate::get(int i) const
{
    if((i < 0) || (static_cast<size_t>(i) >= mList.size()))
        return nullptr;

    return mList[static_cast<size_t>(i)].c_str();
}

int MegaStringListPrivate::size() const
{
    return int(mList.size());
}

void MegaStringListPrivate::add(const char *value)
{
    if (value)
    {
        mList.push_back(value);
    }
}

const string_vector& MegaStringListPrivate::getVector() const
{
    return mList;
}

bool operator==(const MegaStringList& lhs, const MegaStringList& rhs)
{
    if (lhs.size() != rhs.size())
    {
        return false;
    }
    for (int i = 0; i < lhs.size(); ++i)
    {
        if (strcmp(lhs.get(i), rhs.get(i)) != 0)
        {
            return false;
        }
    }
    return true;
}


MegaStringListMap* MegaStringListMapPrivate::copy() const
{
    auto map = new MegaStringListMapPrivate;
    for (const auto& pair : mMap)
    {
        map->set(pair.first.get(), pair.second->copy());
    }
    return map;
}

const MegaStringList* MegaStringListMapPrivate::get(const char* key) const
{
    auto key_ptr = std::unique_ptr<const char[]>{key};
    auto iter = mMap.find(key_ptr);
    key_ptr.release();
    if (iter != mMap.end())
    {
        return iter->second.get();
    }
    return nullptr;
}

MegaStringList *MegaStringListMapPrivate::getKeys() const
{
    string_vector list;
    for (const auto& pair : mMap)
    {
        list.push_back(pair.first.get());
    }
    return new MegaStringListPrivate(std::move(list));
}

void MegaStringListMapPrivate::set(const char* key, const MegaStringList* value)
{
    std::unique_ptr<const char[]> key_ptr{MegaApi::strdup(key)};
    mMap[std::move(key_ptr)] = std::unique_ptr<const MegaStringList>{value};
}

int MegaStringListMapPrivate::size() const
{
    return static_cast<int>(mMap.size());
}

bool MegaStringListMapPrivate::Compare::operator()(const std::unique_ptr<const char[]>& rhs,
                                                   const std::unique_ptr<const char[]>& lhs) const
{
    return strcmp(rhs.get(), lhs.get()) < 0;
}


MegaStringTable* MegaStringTablePrivate::copy() const
{
    auto table = new MegaStringTablePrivate;
    for (const auto& value : mTable)
    {
        table->append(value->copy());
    }
    return table;
}

void MegaStringTablePrivate::append(const MegaStringList* value)
{
    mTable.emplace_back(value);
}

const MegaStringList* MegaStringTablePrivate::get(int i) const
{
    if (i >= 0 && i < size())
    {
        return mTable[static_cast<size_t>(i)].get();
    }
    return nullptr;
}

int MegaStringTablePrivate::size() const
{
    return static_cast<int>(mTable.size());
}


MegaNodeListPrivate::MegaNodeListPrivate()
{
    list = NULL;
    s = 0;
}

MegaNodeListPrivate::MegaNodeListPrivate(Node** newlist, int size)
{
    list = NULL; s = size;
    if(!size) return;

    list = new MegaNode*[static_cast<size_t>(size)];
    for(int i=0; i<size; i++)
        list[i] = MegaNodePrivate::fromNode(newlist[i]);
}

MegaNodeListPrivate::MegaNodeListPrivate(const MegaNodeListPrivate *nodeList, bool copyChildren)
{
    s = nodeList->size();
    if (!s)
    {
        list = NULL;
        return;
    }

    list = new MegaNode*[static_cast<size_t>(s)];
    for (int i = 0; i<s; i++)
    {
        MegaNode *node = nodeList->get(i);
        MegaNodePrivate *nodePrivate = new MegaNodePrivate(node);
        MegaNodeListPrivate *children = dynamic_cast<MegaNodeListPrivate *>(node->getChildren());
        if (children && copyChildren)
        {
            nodePrivate->setChildren(new MegaNodeListPrivate(children, true));
        }
        list[i] = nodePrivate;
    }
}

MegaNodeListPrivate::MegaNodeListPrivate(sharedNode_vector& v)
{
    list = NULL;
    s = static_cast<int>(v.size());
    if (!s) return;

    list = new MegaNode*[static_cast<size_t>(s)];
    for (int i = 0; i < s; i++)
        list[i] = MegaNodePrivate::fromNode(v[static_cast<size_t>(i)].get());
}

MegaNodeListPrivate::MegaNodeListPrivate(sharedNode_list& l)
{
    list = NULL;
    s = static_cast<int>(l.size());
    if (!s) return;

    list = new MegaNode*[static_cast<size_t>(s)];
    int i = 0;
    for (auto& node : l)
    {
        list[i] = MegaNodePrivate::fromNode(node.get());
        i++;
    }
}

MegaNodeListPrivate::~MegaNodeListPrivate()
{
    if(!list)
        return;

    for(int i=0; i<s; i++)
        delete list[i];
    delete [] list;
}

MegaNodeList *MegaNodeListPrivate::copy() const
{
    return new MegaNodeListPrivate(this);
}

MegaNode *MegaNodeListPrivate::get(int i) const
{
    if(!list || (i < 0) || (i >= s))
        return NULL;

    return list[i];
}

int MegaNodeListPrivate::size() const
{
    return s;
}


void MegaNodeListPrivate::addNode(std::unique_ptr<MegaNode> node)
{
    MegaNode** copyList = list;
    s = s + 1;
    list = new MegaNode*[static_cast<size_t>(s)];
    for (int i = 0; i < s - 1; ++i)
    {
        list[i] = copyList[i];
    }

    list[s - 1] = node.release();

    if (copyList != NULL)
    {
        delete [] copyList;
    }
}

void MegaNodeListPrivate::addNode(MegaNode *node)
{
    MegaNode** copyList = list;
    s = s + 1;
    list = new MegaNode*[static_cast<size_t>(s)];
    for (int i = 0; i < s - 1; ++i)
    {
        list[i] = copyList[i];
    }

    list[s - 1] = node->copy();

    if (copyList != NULL)
    {
        delete [] copyList;
    }
}

MegaUserListPrivate::MegaUserListPrivate()
{
    list = NULL;
    s = 0;
}

MegaUserListPrivate::MegaUserListPrivate(User** newlist, int size)
{
    list = NULL;
    s = size;

    if(!size)
        return;

    list = new MegaUser*[static_cast<size_t>(size)];
    for(int i=0; i<size; i++)
        list[i] = MegaUserPrivate::fromUser(newlist[i]);
}

MegaUserListPrivate::MegaUserListPrivate(MegaUserListPrivate *userList)
{
    s = userList->size();
    if (!s)
    {
        list = NULL;
        return;
    }
    list = new MegaUser*[static_cast<size_t>(s)];
    for (int i = 0; i<s; i++)
        list[i] = new MegaUserPrivate(userList->get(i));
}

MegaUserListPrivate::~MegaUserListPrivate()
{
    if(!list)
        return;

    for(int i=0; i<s; i++)
        delete list[i];

    delete [] list;
}

MegaUserList *MegaUserListPrivate::copy()
{
    return new MegaUserListPrivate(this);
}

MegaUser *MegaUserListPrivate::get(int i)
{
    if(!list || (i < 0) || (i >= s))
        return NULL;

    return list[i];
}

int MegaUserListPrivate::size()
{
    return s;
}

MegaUserAlertListPrivate::MegaUserAlertListPrivate()
{
    list = NULL;
    s = 0;
}

MegaUserAlertListPrivate::MegaUserAlertListPrivate(UserAlert::Base** newlist, int size, MegaClient* mc)
{
    list = NULL;
    s = size;

    if (!size)
        return;

    list = new MegaUserAlert*[static_cast<size_t>(size)];
    for (int i = 0; i < size; i++)
    {
        list[i] = new MegaUserAlertPrivate(newlist[i], mc);
    }
}

MegaUserAlertListPrivate::MegaUserAlertListPrivate(const MegaUserAlertListPrivate &userList)
{
    s = userList.size();
    list = s ? new MegaUserAlert*[static_cast<size_t>(s)] : NULL;
    for (int i = 0; i < s; ++i)
    {
        list[i] = userList.get(i)->copy();
    }
}

MegaUserAlertListPrivate::~MegaUserAlertListPrivate()
{
    for (int i = 0; i < s; i++)
    {
        delete list[i];
    }
    delete[] list;
}

MegaUserAlertList *MegaUserAlertListPrivate::copy() const
{
    return new MegaUserAlertListPrivate(*this);
}

MegaUserAlert *MegaUserAlertListPrivate::get(int i) const
{
    if (!list || (i < 0) || (i >= s))
        return NULL;

    return list[i];
}

int MegaUserAlertListPrivate::size() const
{
    return s;
}

void MegaUserAlertListPrivate::clear()
{
    delete[] list;
    s = 0;
    list = nullptr;
}

MegaRecentActionBucketPrivate::MegaRecentActionBucketPrivate(recentaction&& ra)
{
    mData.timestamp = ra.time;
    mData.meta = std::move(ra.meta);
    mData.id = std::move(ra.id);
    mData.nodes = new MegaNodeListPrivate(ra.nodes);
}

MegaRecentActionBucketPrivate::MegaRecentActionBucketPrivate(const BucketData& data)
{
    mData.timestamp = data.timestamp;
    mData.meta = data.meta;
    mData.id = data.id;
    mData.nodes = data.nodes ? data.nodes->copy() : nullptr;
}

MegaRecentActionBucketPrivate::~MegaRecentActionBucketPrivate()
{
    delete mData.nodes;
}

MegaRecentActionBucket *MegaRecentActionBucketPrivate::copy() const
{
    return new MegaRecentActionBucketPrivate(mData);
}

int64_t MegaRecentActionBucketPrivate::getTimestamp() const
{
    return mData.timestamp;
}

const char* MegaRecentActionBucketPrivate::getUserEmail() const
{
    return mData.meta.userEmail.c_str();
}

MegaHandle MegaRecentActionBucketPrivate::getParentHandle() const
{
    return mData.meta.parent;
}

bool MegaRecentActionBucketPrivate::isUpdate() const
{
    return mData.meta.updated;
}

bool MegaRecentActionBucketPrivate::isMedia() const
{
    return mData.meta.media;
}

const char* MegaRecentActionBucketPrivate::getId() const
{
    return mData.id.c_str();
}

const MegaNodeList* MegaRecentActionBucketPrivate::getNodes() const
{
    return mData.nodes;
}

MegaRecentActionBucketListPrivate::MegaRecentActionBucketListPrivate()
{
    list = NULL;
    s = 0;
}

MegaRecentActionBucketListPrivate::MegaRecentActionBucketListPrivate(recentactions_vector& v)
{
    list = NULL;
    s = static_cast<int>(v.size());

    if (!s)
        return;

    list = new MegaRecentActionBucketPrivate*[static_cast<size_t>(s)];
    for (int i = 0; i < s; i++)
    {
        list[i] = new MegaRecentActionBucketPrivate(std::move(v[static_cast<size_t>(i)]));
    }
}

MegaRecentActionBucketListPrivate::MegaRecentActionBucketListPrivate(const MegaRecentActionBucketListPrivate &o)
{
    s = o.size();
    list = s ? new MegaRecentActionBucketPrivate*[static_cast<size_t>(s)] : NULL;
    for (int i = 0; i < s; ++i)
    {
        list[i] = (MegaRecentActionBucketPrivate*)o.get(i)->copy();
    }
}

MegaRecentActionBucketListPrivate::~MegaRecentActionBucketListPrivate()
{
    for (int i = 0; i < s; i++)
    {
        delete list[i];
    }
    delete[] list;
}

MegaRecentActionBucketList *MegaRecentActionBucketListPrivate::copy() const
{
    return new MegaRecentActionBucketListPrivate(*this);
}

MegaRecentActionBucket *MegaRecentActionBucketListPrivate::get(int i) const
{
    if (!list || (i < 0) || (i >= s))
    {
        return NULL;
    }
    return list[i];
}

int MegaRecentActionBucketListPrivate::size() const
{
    return s;
}

MegaShareListPrivate::MegaShareListPrivate()
{
    list = NULL;
    s = 0;
}

MegaShareListPrivate::MegaShareListPrivate(const std::vector<impl::ShareData>& shares)
{
    // Convinence
    const auto size = shares.size();

    // Default
    list = nullptr;
    s = static_cast<int>(size);

    // Empty
    if (!size)
    {
        return;
    }

    // Construct list if it is not empty
    list = new MegaShare*[size];
    for (size_t i = 0; i < size; i++)
    {
        list[i] = MegaSharePrivate::fromShare(shares[i]);
    }
}

MegaShareListPrivate::~MegaShareListPrivate()
{
    if(!list)
        return;

    for(int i=0; i<s; i++)
        delete list[i];

    delete [] list;
}

MegaShare *MegaShareListPrivate::get(int i)
{
    if(!list || (i < 0) || (i >= s))
        return NULL;

    return list[i];
}

int MegaShareListPrivate::size()
{
    return s;
}

MegaTransferListPrivate::MegaTransferListPrivate()
{
    list = NULL;
    s = 0;
}

MegaTransferListPrivate::MegaTransferListPrivate(MegaTransfer** newlist, int size)
{
    list = NULL;
    s = size;

    if(!size)
        return;

    list = new MegaTransfer*[static_cast<size_t>(size)];
    for(int i=0; i<size; i++)
        list[i] = newlist[i]->copy();
}

MegaTransferListPrivate::~MegaTransferListPrivate()
{
    if(!list)
        return;

    for(int i=0; i < s; i++)
        delete list[i];

    delete [] list;
}

MegaTransfer *MegaTransferListPrivate::get(int i)
{
    if(!list || (i < 0) || (i >= s))
        return NULL;

    return list[i];
}

int MegaTransferListPrivate::size()
{
    return s;
}

MegaContactRequestListPrivate::MegaContactRequestListPrivate()
{
    list = NULL;
    s = 0;
}

MegaContactRequestListPrivate::MegaContactRequestListPrivate(PendingContactRequest **newlist, int size)
{
    list = NULL;
    s = size;

    if(!size)
        return;

    list = new MegaContactRequest*[static_cast<size_t>(size)];
    for(int i=0; i<size; i++)
        list[i] = new MegaContactRequestPrivate(newlist[i]);
}

MegaContactRequestListPrivate::~MegaContactRequestListPrivate()
{
    if(!list)
        return;

    for(int i=0; i < s; i++)
        delete list[i];

    delete [] list;
}

MegaContactRequestList* MegaContactRequestListPrivate::copy() const
{
    return new MegaContactRequestListPrivate(this);
}

const MegaContactRequest* MegaContactRequestListPrivate::get(int i) const
{
    if(!list || (i < 0) || (i >= s))
        return NULL;

    return list[i];
}

int MegaContactRequestListPrivate::size() const
{
    return s;
}

MegaContactRequestListPrivate::MegaContactRequestListPrivate(
    const MegaContactRequestListPrivate* requestList)
{
    s = requestList->size();
    if (!s)
    {
        list = NULL;
        return;
    }
    list = new MegaContactRequest*[static_cast<size_t>(s)];
    for (int i = 0; i < s; i++)
        list[i] = new MegaContactRequestPrivate(requestList->get(i));
}

MegaFile::MegaFile() : File()
{
    megaTransfer = NULL;
}

void MegaFile::setTransfer(MegaTransferPrivate* newTransfer)
{
    megaTransfer = newTransfer;
}

MegaTransferPrivate *MegaFile::getTransfer()
{
    return megaTransfer;
}

bool MegaFile::serialize(string *d) const
{
    if (!megaTransfer)
    {
        return false;
    }

    if (!File::serialize(d))
    {
        return false;
    }

    megaTransfer->dbid = dbid;
    if (!megaTransfer->serialize(d))
    {
        return false;
    }

    d->append("\0\0\0\0\0\0\0", 8);

    return true;
}

MegaFile *MegaFile::unserialize(string *d)
{
    File *file = File::unserialize(d);
    if (!file)
    {
        LOG_err << "Error unserializing MegaFile: Unable to unserialize File";
        return NULL;
    }

    MegaFile *megaFile = new MegaFile();
    *(File *)megaFile = *(File *)file;
    file->chatauth = NULL;
    delete file;

    MegaTransferPrivate *transfer = MegaTransferPrivate::unserialize(d);
    if (!transfer)
    {
        delete megaFile;
        return NULL;
    }
    else
    {
        transfer->dbid = megaFile->dbid;
    }

    const char* ptr = d->data();
    const char* end = ptr + d->size();
    if (ptr + 8 > end)
    {
        LOG_err << "MegaFile unserialization failed - data too short";
        delete megaFile;
        delete transfer;
        return NULL;
    }

    if (memcmp(ptr, "\0\0\0\0\0\0\0", 8))
    {
        LOG_err << "MegaFile unserialization failed - invalid version";
        delete megaFile;
        delete transfer;
        return NULL;
    }
    ptr += 8;

    d->erase(0, static_cast<size_t>(ptr - d->data()));

    transfer->setSourceFileTemporary(megaFile->temporaryfile);

    megaFile->setTransfer(transfer);
    return megaFile;
}

MegaFileGet::MegaFileGet(MegaClient *client, Node *n, const LocalPath& dstPath, FileSystemType fsType, CollisionResolution collisionResolution) : MegaFile()
{
    setCollisionResolution(collisionResolution);

    h = n->nodeHandle();
    *(FileFingerprint*)this = *n;

    name = n->displayname();
    auto lpName = LocalPath::fromRelativeName(name, *client->fsaccess, fsType);

    LocalPath finalPath;
    if(!dstPath.empty())
    {
        if (dstPath.endsInSeparator())
        {
            finalPath = dstPath;
            finalPath.appendWithSeparator(lpName, true);
        }
        else finalPath = dstPath;
    }
    else
        finalPath = lpName;

    size = n->size;
    mtime = n->mtime;

    if (n->nodekey().size() == sizeof(filekey))
        memcpy(filekey,n->nodekey().data(),sizeof filekey);

    setLocalname(finalPath);
    hprivate = true;
    hforeign = false;
}

MegaFileGet::MegaFileGet(MegaClient *client, MegaNode *n, const LocalPath& dstPath, CollisionResolution collisionResolution) : MegaFile()
{
    setCollisionResolution(collisionResolution);

    h.set6byte(n->getHandle());

    FileSystemType fsType = client->fsaccess->getlocalfstype(dstPath);

    name = n->getName();
    auto lpName = LocalPath::fromRelativeName(name, *client->fsaccess, fsType);

    LocalPath finalPath;
    if(!dstPath.empty())
    {
        if (dstPath.endsInSeparator())
        {
            finalPath = dstPath;
            finalPath.appendWithSeparator(lpName, true);
        }
        else finalPath = dstPath;
    }
    else finalPath = lpName;

    const char *fingerprint = n->getFingerprint();
    if (fingerprint)
    {
        unique_ptr<FileFingerprint> fp(MegaApiImpl::getFileFingerprintInternal(fingerprint));
        if (fp)
        {
            *(FileFingerprint *)this = *(FileFingerprint *)fp.get();
        }
    }

    size = n->getSize();
    mtime = n->getModificationTime();

    if (n->getNodeKey()->size() == sizeof(filekey))
        memcpy(filekey,n->getNodeKey()->data(),sizeof filekey);

    setLocalname(finalPath);
    hprivate = !n->isPublic();
    hforeign = n->isForeign();

    MegaNodePrivate* np = dynamic_cast<MegaNodePrivate*>(n);
    assert(np);
    if (np->getPrivateAuth()->size())
    {
        privauth = *np->getPrivateAuth();
    }

    if (np->getPublicAuth()->size())
    {
        pubauth = *np->getPublicAuth();
    }

    chatauth = np->getChatAuth() ? MegaApi::strdup(np->getChatAuth()) : NULL;
}

bool MegaFileGet::serialize(string *d) const
{
    if (!MegaFile::serialize(d))
    {
        return false;
    }

    CacheableWriter cw(*d);
    cw.serializeexpansionflags(mUndelete);

    return true;
}

MegaFileGet *MegaFileGet::unserialize(string *d)
{
    MegaFile *file = MegaFile::unserialize(d);
    if (!file)
    {
        LOG_err << "Error unserializing MegaFileGet: Unable to unserialize MegaFile";
        return NULL;
    }

    const char* ptr = d->data();
    const char* end = ptr + d->size();
    if (ptr + 8 > end)
    {
        LOG_err << "MegaFileGet unserialization failed - data too short";
        delete file;
        return NULL;
    }

    byte expansions[8];
    CacheableReader cr(*d);
    if (!cr.unserializeexpansionflags(expansions, 1))
    {
        LOG_err << "MegaFileGet unserialization failed - invalid version";
        delete file;
        return NULL;
    }

    MegaFileGet *megaFile = new MegaFileGet();
    *(MegaFile *)megaFile = *(MegaFile *)file;
    megaFile->setUndelete(expansions[0] > 0);
    file->chatauth = NULL;
    delete file;

    return megaFile;
}

void MegaFileGet::prepare(FileSystemAccess&)
{
    if (transfer->localfilename.empty())
    {
        transfer->localfilename = getLocalname();
        assert(transfer->localfilename.isAbsolute() || transfer->localfilename.isURI());
        transfer->localfilename.changeLeaf(LocalPath::tmpNameLocal());
    }
}

void MegaFileGet::updatelocalname()
{
#ifdef _WIN32
    RemoveHiddenFileAttribute(transfer->localfilename);
#endif
}

void MegaFileGet::progress()
{
#ifdef _WIN32
    if(transfer->slot && !transfer->slot->progressreported)
    {
        AddHiddenFileAttribute(transfer->localfilename);
    }
#endif
}

void MegaFileGet::completed(Transfer*, putsource_t /*source*/)
{
    delete this;
}

void MegaFileGet::terminated(error)
{
    delete this;
}

MegaFilePut::MegaFilePut(MegaClient *, LocalPath clocalname, string *filename, NodeHandle ch, const char* ctargetuser, int64_t mtime, bool isSourceTemporary, std::shared_ptr<Node> pvNode)

    : MegaFile()
{
    // full local path
    setLocalname(clocalname);

    // target parent node
    h = ch;

    // target user
    targetuser = ctargetuser;

    // new node name
    name = *filename;

    // If the file's time is to be used, this is MegaApi::INVALID_CUSTOM_MOD_TIME
    customMtime = mtime;

    temporaryfile = isSourceTemporary;

    previousNode = pvNode;
}

bool MegaFilePut::serialize(string *d) const
{
    if (!MegaFile::serialize(d))
    {
        return false;
    }

    d->append((char*)&customMtime, sizeof(customMtime));
    d->append("\0\0\0\0\0\0\0", 8);

    return true;
}

MegaFilePut *MegaFilePut::unserialize(string *d)
{
    MegaFile *file = MegaFile::unserialize(d);
    if (!file)
    {
        LOG_err << "Error unserializing MegaFilePut: Unable to unserialize MegaFile";
        return NULL;
    }

    const char* ptr = d->data();
    const char* end = ptr + d->size();
    if (ptr + sizeof(int64_t) + 8 > end)
    {
        LOG_err << "MegaFilePut unserialization failed - data too short";
        delete file;
        return NULL;
    }

    int64_t customMtime = MemAccess::get<int64_t>(ptr);
    ptr += sizeof(customMtime);

    if (memcmp(ptr, "\0\0\0\0\0\0\0", 8))
    {
        LOG_err << "MegaFilePut unserialization failed - invalid version";
        delete file;
        return NULL;
    }

    ptr += 8;
    if (ptr != end)
    {
        LOG_err << "MegaFilePut unserialization failed - wrong size";
        delete file;
        return NULL;
    }

    MegaFilePut *megaFile = new MegaFilePut();
    *(MegaFile *)megaFile = *(MegaFile *)file;
    file->chatauth = NULL;
    delete file;

    megaFile->customMtime = customMtime;
    return megaFile;
}

void MegaFilePut::completed(Transfer* t, putsource_t source)
{
    assert(!transfer || t == transfer);
    assert(source == PUTNODES_APP);
    assert(t->type == PUT);

    // allow for putnodes with a different mtime to the actual file
    sendPutnodesOfUpload(t->client,
                         t->uploadhandle,
                         "",
                         *t->ultoken,
                         t->filekey,
                         source,
                         NodeHandle(),
                         nullptr,
                         customMtime == MegaApi::INVALID_CUSTOM_MOD_TIME ? nullptr : &customMtime,
                         false,
                         getPitag());

    delete this;
}

void MegaFilePut::terminated(error)
{
    delete this;
}


void MegaSearchFilterPrivate::byName(const char* searchString)
{
    mNameFilter = searchString ? searchString : string();
}

void MegaSearchFilterPrivate::byNodeType(int nodeType)
{
    assert(MegaNode::TYPE_UNKNOWN <= nodeType && nodeType <= MegaNode::TYPE_FOLDER);
    if (nodeType < MegaNode::TYPE_UNKNOWN || MegaNode::TYPE_FOLDER < nodeType)
    {
        LOG_warn << "Invalid nodeType for SearchFilter: " << nodeType << ". Ignored.";
        return;
    }

    mNodeType = nodeType;
}

void MegaSearchFilterPrivate::byCategory(int mimeType)
{
    assert(MegaApi::FILE_TYPE_DEFAULT <= mimeType && mimeType <= MegaApi::FILE_TYPE_LAST);
    if (mimeType < MegaApi::FILE_TYPE_DEFAULT || MegaApi::FILE_TYPE_LAST < mimeType)
    {
        LOG_warn << "Invalid mimeType for SearchFilter: " << mimeType << ". Ignored.";
        return;
    }

    mMimeCategory = mimeType;
}

void MegaSearchFilterPrivate::byFavourite(int boolFilterOption)
{
    mFavouriteFilterOption = validateBoolFilterOption(boolFilterOption);
}

void MegaSearchFilterPrivate::bySensitivity(int boolFilterOption)
{
    mExcludeSensitive = validateBoolFilterOption(boolFilterOption);
}

void MegaSearchFilterPrivate::byLocationHandle(MegaHandle ancestorHandle)
{
    mLocationHandle = ancestorHandle;
    mLocationType = MegaApi::SEARCH_TARGET_ALL;
}

void MegaSearchFilterPrivate::byLocation(int locationType)
{
    assert(MegaApi::SEARCH_TARGET_INSHARE <= locationType && locationType <= MegaApi::SEARCH_TARGET_ALL);
    if (locationType < MegaApi::SEARCH_TARGET_INSHARE || MegaApi::SEARCH_TARGET_ALL < locationType)
    {
        LOG_warn << "Invalid locationType for SearchFilter: " << locationType << ". Ignored.";
        return;
    }

    mLocationType = locationType;
    mLocationHandle = INVALID_HANDLE;
}

void MegaSearchFilterPrivate::byCreationTime(int64_t lowerLimit, int64_t upperLimit)
{
    mCreationLowerLimit = lowerLimit;
    mCreationUpperLimit = upperLimit;
}

void MegaSearchFilterPrivate::byModificationTime(int64_t lowerLimit, int64_t upperLimit)
{
    mModificationLowerLimit = lowerLimit;
    mModificationUpperLimit = upperLimit;
}

void MegaSearchFilterPrivate::byDescription(const char* searchString)
{
    mDescriptionFilter = searchString ? searchString : string();
}

void MegaSearchFilterPrivate::byTag(const char* searchString)
{
    mTag = searchString ? searchString : string();
}

void MegaSearchFilterPrivate::useAndForTextQuery(bool useAnd)
{
    mUseAndForTextQuery = useAnd;
}

MegaSearchFilterPrivate* MegaSearchFilterPrivate::copy() const
{
    return new MegaSearchFilterPrivate(*this);
}

int MegaSearchFilterPrivate::validateBoolFilterOption(const int value)
{
    switch (value)
    {
    case MegaSearchFilter::BOOL_FILTER_DISABLED:
    case MegaSearchFilter::BOOL_FILTER_ONLY_TRUE:
    case MegaSearchFilter::BOOL_FILTER_ONLY_FALSE:
        return value;
    default:
        LOG_warn << "Invalid value for a boolean filtering option: " << value;
        return MegaSearchFilter::BOOL_FILTER_DISABLED;
    }
}

// Pin the public SECTION_GRANULARITY_* values to the internal enum. Same pattern
// as the LOCATION_* asserts; bumping either side without the other would cause
// a silent reinterpret on the boundary.
static_assert(static_cast<int>(DateSectionGranularity::Day) ==
                  MegaGroupNodesByDateFilter::SECTION_GRANULARITY_DAY,
              "DateSectionGranularity::Day must mirror SECTION_GRANULARITY_DAY");
static_assert(static_cast<int>(DateSectionGranularity::Month) ==
                  MegaGroupNodesByDateFilter::SECTION_GRANULARITY_MONTH,
              "DateSectionGranularity::Month must mirror SECTION_GRANULARITY_MONTH");
static_assert(static_cast<int>(DateSectionGranularity::Year) ==
                  MegaGroupNodesByDateFilter::SECTION_GRANULARITY_YEAR,
              "DateSectionGranularity::Year must mirror SECTION_GRANULARITY_YEAR");

static bool isSupportedTimestampOrder(int order)
{
    return order == MegaApi::ORDER_MODIFICATION_ASC || order == MegaApi::ORDER_MODIFICATION_DESC;
}

void MegaListAllNodesFilterPrivate::byTimestampAnchor(int64_t startDate,
                                                      int64_t endDate,
                                                      int sectionOrder)
{
    if ((startDate == 0 && endDate == 0) || sectionOrder == -1)
    {
        startDate = 0;
        endDate = 0;
        sectionOrder = -1;
    }

    mTimestampAnchorStartDate = startDate;
    mTimestampAnchorEndDate = endDate;
    mTimestampAnchorOrder = sectionOrder;
}

std::unique_ptr<MegaGfxProviderPrivate>
    MegaGfxProviderPrivate::createIsolatedInstance([[maybe_unused]] const char* endpointName,
                                                   [[maybe_unused]] const char* executable,
                                                   [[maybe_unused]] unsigned int keepAliveInSeconds,
                                                   [[maybe_unused]] const MegaStringList* extraArgs)
{
#ifdef ENABLE_ISOLATED_GFX
    if (!endpointName || !executable)
        return nullptr;

    auto args = dynamic_cast<const MegaStringListPrivate*>(extraArgs);
    GfxIsolatedProcess::Params params{std::string{endpointName},
                                      std::string{executable},
                                      std::chrono::seconds{keepAliveInSeconds},
                                      args ? args->getVector() : string_vector{}};
    auto provider = GfxProviderIsolatedProcess::create(params);
    return std::make_unique<MegaGfxProviderPrivate>(std::move(provider));
#else
    return nullptr;
#endif
}

std::unique_ptr<MegaGfxProviderPrivate> MegaGfxProviderPrivate::createExternalInstance(MegaGfxProcessor* processor)
{
    return std::make_unique<MegaGfxProviderPrivate>(std::make_unique<GfxProviderExternal>(processor));
}

std::unique_ptr<MegaGfxProviderPrivate> MegaGfxProviderPrivate::createInternalInstance()
{
    return std::make_unique<MegaGfxProviderPrivate>(IGfxProvider::createInternalGfxProvider());
}

//Entry point for the blocking thread
void *MegaApiImpl::threadEntryPoint(void *param)
{
#ifndef _WIN32
    struct sigaction noaction;
    memset(&noaction, 0, sizeof(noaction));
    noaction.sa_handler = SIG_IGN;
    ::sigaction(SIGPIPE, &noaction, 0);
#endif

    MegaApiImpl *megaApiImpl = (MegaApiImpl *)param;
    megaApiImpl->loop();
    return 0;
}

MegaTransferPrivate *MegaApiImpl::getMegaTransferPrivate(int tag)
{
    map<int, MegaTransferPrivate *>::iterator it = transferMap.find(tag);
    if (it == transferMap.end())
    {
        return NULL;
    }
    return it->second;
}

MegaApiImpl::MegaApiImpl(MegaApi* api,
                         MegaGfxProcessor* processor,
                         const char* basePath,
                         const char* userAgent,
                         unsigned workerThreadCount,
                         int clientType)
{
    init(api, createGfxProc(processor), basePath, userAgent, workerThreadCount, clientType);
}

MegaApiImpl::MegaApiImpl(MegaApi* api,
                         MegaGfxProvider* provider,
                         const char* basePath,
                         const char* userAgent,
                         unsigned workerThreadCount,
                         int clientType)
{
    auto p = dynamic_cast<MegaGfxProviderPrivate*>(provider);
    auto iProvider = p ? p->releaseProvider() : nullptr;
    auto gfxproc = iProvider ? std::make_unique<GfxProc>(std::move(iProvider)) : nullptr;
    init(api, std::move(gfxproc), basePath, userAgent, workerThreadCount, clientType);
}

void MegaApiImpl::init(MegaApi* publicApi,
                       std::unique_ptr<GfxProc> gfxproc,
                       const char* newBasePath,
                       const char* userAgent,
                       unsigned clientWorkerThreadCount,
                       int clientType)
{
    api = publicApi;

    maxRetries = 7;
    currentTransfer = NULL;
    client = NULL;
    waitingRequest = RETRY_NONE;
    notificationNumber = 0;

#ifdef HAVE_LIBUV
    httpServer = NULL;
    httpServerMaxBufferSize = 0;
    httpServerMaxOutputSize = 0;
    httpServerThrottleBitrateBps = 0;
    httpServerEnableFiles = true;
    httpServerEnableFolders = false;
    httpServerOfflineAttributeEnabled = false;
    httpServerRestrictedMode = MegaApi::TCP_SERVER_ALLOW_CREATED_LOCAL_LINKS;
    httpServerSubtitlesSupportEnabled = false;

    ftpServer = NULL;
    ftpServerMaxBufferSize = 0;
    ftpServerMaxOutputSize = 0;
    ftpServerRestrictedMode = MegaApi::TCP_SERVER_ALLOW_CREATED_LOCAL_LINKS;
    const char *uvversion = uv_version_string();
    if (uvversion)
    {
        LOG_debug << "libuv version: " <<uvversion;
    }
#endif

    mTimezones = NULL;

    httpio = new MegaHttpIO();
    waiter.reset(new MegaWaiter());

    fsAccess = mega::createFSA();
    fingerprintingFsAccess = mega::createFSA();

    if (newBasePath)
    {
        basePath = newBasePath;
    }
    else
    {
        basePath = std::filesystem::current_path().string();
    }
    dbAccess = new MegaDbAccess(LocalPath::fromAbsolutePath(basePath));

    gfxAccess = gfxproc.release();
    if (gfxAccess)
    {
        gfxAccess->startProcessingThread();
    }

    if(!userAgent)
    {
        userAgent = "";
    }

    nocache = false;
    client = new MegaClient(this,
                            waiter,
                            httpio,
                            dbAccess,
                            gfxAccess,
                            userAgent,
                            clientWorkerThreadCount,
                            MegaClient::ClientType(clientType));

#if defined(_WIN32)
    httpio->unlock();
#endif

    //Start blocking thread
    threadExit = 0;
    thread = std::thread([this](){ threadEntryPoint(this); } );
    threadId = thread.get_id();
}

MegaApiImpl::~MegaApiImpl()
{
    // the fireOnFinish won't be called for this one, so delete it ourselves
    auto shutdownRequest = std::make_unique<MegaRequestPrivate>(MegaRequest::TYPE_DELETE);

    requestQueue.push(shutdownRequest.get());
    waiter->notify();
    thread.join();
    assert(client == nullptr);

    delete mTimezones;

    assert(requestMap.empty());
    assert(backupsMap.empty());
    assert(transferMap.empty());

    delete gfxAccess;

#ifndef DONT_RELEASE_HTTPIO
    delete httpio;
#endif
}

MegaApiImpl* MegaApiImpl::ImplOf(MegaApi* api)
{
    // Sometimes we need to be able to reference the MegaApiImpl from objects other than MegaApi (without giving clients access to the pImpl pointer)
    return api->pImpl;
}

void MegaApiImpl::loggedInStateChanged(sessiontype_t s, handle me, const string& email)
{
    std::lock_guard<std::mutex> g(mLastRecievedLoggedMeMutex);
    mLastReceivedLoggedInState = s;
    mLastReceivedLoggedInMeHandle = me;
    mLastReceivedLoggedInMyEmail = email;
}

int MegaApiImpl::isLoggedIn()
{
    std::lock_guard<std::mutex> g(mLastRecievedLoggedMeMutex);
    return mLastReceivedLoggedInState;
}

bool MegaApiImpl::isEphemeralPlusPlus()
{
    return isLoggedIn() == EPHEMERALACCOUNTPLUSPLUS;
}

char* MegaApiImpl::getMyEmail()
{
    std::unique_lock<mutex> g(mLastRecievedLoggedMeMutex);

    if (mLastReceivedLoggedInState == NOTLOGGEDIN ||
            mLastReceivedLoggedInMyEmail.empty())
    {
        return nullptr;
    }

    return MegaApi::strdup(mLastReceivedLoggedInMyEmail.c_str());
}

int64_t MegaApiImpl::getAccountCreationTs()
{
    return client->accountsince;
}

char *MegaApiImpl::getMyUserHandle()
{
    std::lock_guard<std::mutex> g(mLastRecievedLoggedMeMutex);

    if (mLastReceivedLoggedInState == NOTLOGGEDIN ||
        ISUNDEF(mLastReceivedLoggedInMeHandle))
    {
        return NULL;
    }

    char buf[12];
    Base64::btoa((const byte*)&mLastReceivedLoggedInMeHandle, MegaClient::USERHANDLE, buf);
    char *result = MegaApi::strdup(buf);
    return result;
}

MegaHandle MegaApiImpl::getMyUserHandleBinary()
{
    SdkMutexGuard g(sdkMutex);
    return client->me;
}

MegaUser *MegaApiImpl::getMyUser()
{
    SdkMutexGuard g(sdkMutex);
    return MegaUserPrivate::fromUser(client->finduser(client->me));
}

bool MegaApiImpl::isAchievementsEnabled()
{
    assert(!isBusinessAccount() || !client->achievements_enabled);
    return client->achievements_enabled;
}

bool MegaApiImpl::isProFlexiAccount()
{
    return client->isProFlexi();
}

bool MegaApiImpl::isBusinessAccount()
{
    return client->mBizStatus != BIZ_STATUS_INACTIVE
             && client->mBizStatus != BIZ_STATUS_UNKNOWN;
}

bool MegaApiImpl::isMasterBusinessAccount()
{
    return client->mBizMode == BIZ_MODE_MASTER;
}

bool MegaApiImpl::isBusinessAccountActive()
{
    return getBusinessStatus() >= BIZ_STATUS_ACTIVE;
}

int MegaApiImpl::getBusinessStatus()
{
    // Prevent return apps unknown status
    return (client->mBizStatus == BIZ_STATUS_UNKNOWN)
            ? BIZ_STATUS_INACTIVE
            : client->mBizStatus;
}

int64_t MegaApiImpl::getOverquotaDeadlineTs()
{
    return client->mOverquotaDeadlineTs;
}

MegaIntegerList *MegaApiImpl::getOverquotaWarningsTs()
{
    return new MegaIntegerListPrivate(client->mOverquotaWarningTs);
}

bool MegaApiImpl::checkPassword(const char *password)
{
    SdkMutexGuard g(sdkMutex);
    return client->validatepwdlocally(password);
}

char *MegaApiImpl::getMyCredentials()
{
    SdkMutexGuard g(sdkMutex);
    if (ISUNDEF(client->me))
    {
        return NULL;
    }

    string result;
    if (client->mEd255Key)
    {
        result = AuthRing::fingerprint(
            string((const char*)client->mEd255Key->pubKey, EdDSA::PUBLIC_KEY_LENGTH),
            true);
    }

    return result.size() ? MegaApi::strdup(result.c_str()) : nullptr;
}

void MegaApiImpl::getUserCredentials(MegaUser *user, MegaRequestListener *listener)
{
    MegaRequestPrivate *request = new MegaRequestPrivate(MegaRequest::TYPE_GET_ATTR_USER, listener);

    request->setParamType(ATTR_ED25519_PUBK);
    request->setFlag(true);
    if(user)
    {
        request->setEmail(user->getEmail());
    }

    request->performRequest = [this, request]()
    {
        return performRequest_getAttrUser(request);
    };

    requestQueue.push(request);
    waiter->notify();
}

bool MegaApiImpl::areCredentialsVerified(MegaUser *user)
{
    SdkMutexGuard g(sdkMutex);
    return user ? client->areCredentialsVerified(user->getHandle()) : false;
}

void MegaApiImpl::verifyCredentials(MegaUser *user, MegaRequestListener *listener)
{
    MegaRequestPrivate *request = new MegaRequestPrivate(MegaRequest::TYPE_VERIFY_CREDENTIALS, listener);

    if(user)
    {
        request->setNodeHandle(user->getHandle());
    }

    request->performRequest = [this, request]()
    {
        return performRequest_verifyCredentials(request);
    };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::resetCredentials(MegaUser *user, MegaRequestListener *listener)
{
    MegaRequestPrivate *request = new MegaRequestPrivate(MegaRequest::TYPE_VERIFY_CREDENTIALS, listener);

    if(user)
    {
        request->setNodeHandle(user->getHandle());
    }
    request->setFlag(true);

    request->performRequest = [this, request]()
    {
        return performRequest_verifyCredentials(request);
    };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::setLogExtraForModules(bool networking, [[maybe_unused]] bool syncs)
{
    g_netLoggingOn = networking;
#ifdef ENABLE_SYNC
    client->syncs.mDetailedSyncLogging = syncs;
#endif
}

void MegaApiImpl::setLogLevel(int logLevel)
{
    SimpleLogger::setLogLevel(LogLevel(logLevel));
}

void MegaApiImpl::setMaxPayloadLogSize(size_t maxSize)
{
    SimpleLogger::setMaxPayloadLogSize(maxSize);
}

void MegaApiImpl::addLoggerClass(MegaLogger *megaLogger, bool singleExclusiveLogger)
{

    if (singleExclusiveLogger)
    {
        assert(!getExclusiveLogger().exclusiveCallback);
        getExclusiveLogger().exclusiveCallback = [megaLogger](const char* time,
                                                              int loglevel,
                                                              const char* source,
                                                              const char* message
#ifdef ENABLE_LOG_PERFORMANCE
                                                              ,
                                                              const char** directMessages,
                                                              size_t* directMessagesSizes,
                                                              unsigned numberMessages
#endif
                                                 )
        {
            megaLogger->log(time,
                            loglevel,
                            source,
                            message
#ifdef ENABLE_LOG_PERFORMANCE
                            ,
                            directMessages,
                            directMessagesSizes,
                            static_cast<int>(numberMessages)
#endif
            );
        };

        SimpleLogger::setOutputClass(&getExclusiveLogger());
    }
    else
    {
        getExternalLogger().addMegaLogger(megaLogger,
                                          [megaLogger](const char* time,
                                                       int loglevel,
                                                       const char* source,
                                                       const char* message
#ifdef ENABLE_LOG_PERFORMANCE
                                                       ,
                                                       const char** directMessages,
                                                       size_t* directMessagesSizes,
                                                       unsigned numberMessages
#endif
                                          )
                                          {
                                              megaLogger->log(time,
                                                              loglevel,
                                                              source,
                                                              message
#ifdef ENABLE_LOG_PERFORMANCE
                                                              ,
                                                              directMessages,
                                                              directMessagesSizes,
                                                              static_cast<int>(numberMessages)
#endif
                                              );
                                          });
    }
}

void MegaApiImpl::removeLoggerClass(MegaLogger *megaLogger, bool singleExclusiveLogger)
{
    if (singleExclusiveLogger)
    {
        SimpleLogger::setOutputClass(&getExternalLogger());
        getExclusiveLogger().exclusiveCallback = nullptr;
    }
    else
    {
        getExternalLogger().removeMegaLogger(megaLogger);
    }
}

void MegaApiImpl::setLogToConsole(bool enable)
{
    // only supported for external (not exclusive) loggers
    getExternalLogger().setLogToConsole(enable);
}

void MegaApiImpl::setLogJSONContent(bool enable)
{
    JSONLog::set(JSONLog::CHUNK_RECEIVED | JSONLog::CHUNK_CONSUMED | JSONLog::SENDING |
                 JSONLog::NONCHUNK_RECEIVED);

    if (enable)
        SimpleLogger::setMaxPayloadLogSize(0); // Max size
    else
        SimpleLogger::setMaxPayloadLogSize(); // Default
}

void MegaApiImpl::setLogJSON(uint32_t value)
{
    JSONLog::set(value);
}

uint32_t MegaApiImpl::getLogJSON()
{
    return JSONLog::get();
}

void MegaApiImpl::log(int logLevel, const char *message, const char *filename, int line)
{
    SimpleLogger::postLog(LogLevel(logLevel), message, filename, line);
}

void MegaApiImpl::setLoggingName(const char* loggingName)
{
    SdkMutexGuard g(sdkMutex);
    if (loggingName)
    {
        client->clientname = string(loggingName) + " ";
    }
    else
    {
        client->clientname.clear();
    }
}

long long MegaApiImpl::getSDKtime()
{
    return Waiter::ds;
}

MegaHandle MegaApiImpl::base32ToHandle(const char *base32Handle)
{
    if(!base32Handle) return INVALID_HANDLE;

    handle h = 0;
    Base32::atob(base32Handle,(byte*)&h, MegaClient::USERHANDLE);
    return h;
}

const char* MegaApiImpl::ebcEncryptKey(const char* encryptionKey, const char* plainKey)
{
    if(!encryptionKey || !plainKey) return NULL;

    char pwkey[SymmCipher::KEYLENGTH];
    Base64::atob(encryptionKey, (byte *)pwkey, sizeof pwkey);

    SymmCipher key;
    key.setkey((byte*)pwkey);

    char plkey[SymmCipher::KEYLENGTH];
    Base64::atob(plainKey, (byte*)plkey, sizeof plkey);
    key.ecb_encrypt((byte*)plkey);

    char* buf = new char[SymmCipher::KEYLENGTH*4/3+4];
    Base64::btoa((byte*)plkey, SymmCipher::KEYLENGTH, buf);
    return buf;
}

handle MegaApiImpl::base64ToHandle(const char* base64Handle)
{
    if(!base64Handle) return UNDEF;

    handle h = 0;
    Base64::atob(base64Handle,(byte*)&h,MegaClient::NODEHANDLE);
    return h;
}

handle MegaApiImpl::base64ToUserHandle(const char *base64Handle)
{
    if(!base64Handle) return UNDEF;

    handle h = 0;
    Base64::atob(base64Handle,(byte*)&h,MegaClient::USERHANDLE);
    return h;
}

handle MegaApiImpl::base64ToBackupId(const char* backupId)
{
    if (!backupId || !*backupId) return UNDEF;

    handle result = 0x0;

    Base64::atob(backupId,
                 reinterpret_cast<byte*>(&result),
                 MegaClient::BACKUPHANDLE);

    return result;
}

char *MegaApiImpl::handleToBase64(MegaHandle handle)
{
    char *base64Handle = new char[12];
    Base64::btoa((byte*)&(handle),MegaClient::NODEHANDLE,base64Handle);
    return base64Handle;
}

char *MegaApiImpl::userHandleToBase64(MegaHandle handle)
{
    char *base64Handle = new char[14];
    Base64::btoa((byte*)&(handle),MegaClient::USERHANDLE,base64Handle);
    return base64Handle;
}

const char* MegaApiImpl::backupIdToBase64(MegaHandle backupId)
{
    unique_ptr<char[]> result(new char[14]);

    Base64::btoa(reinterpret_cast<byte*>(&backupId),
                 MegaClient::BACKUPHANDLE,
                 result.get());

    return result.release();
}

char *MegaApiImpl::binaryToBase64(const char *binaryData, size_t length)
{
    char *ret = new char[length * 4 / 3 + 3];
    Base64::btoa((byte*)binaryData, length, ret);
    return ret;
}

void MegaApiImpl::base64ToBinary(const char *base64string, unsigned char **binary, size_t* binarysize)
{
    string data;
    data.resize(strlen(base64string) * 3 / 4 + 3);
    data.resize(
        static_cast<size_t>(Base64::atob(base64string, (byte*)data.data(), int(data.size()))));
    *binarysize = data.size();
    *binary = new unsigned char[*binarysize];
    memcpy(*binary, data.data(), *binarysize);
}

void MegaApiImpl::retryPendingConnections(bool disconnect, bool includexfers, MegaRequestListener *listener)
{
    MegaRequestPrivate *request = new MegaRequestPrivate(MegaRequest::TYPE_RETRY_PENDING_CONNECTIONS, listener);
    request->setFlag(disconnect);
    request->setNumber(includexfers);

    request->performRequest = [this, request]()
    {
        return performRequest_retryPendingConnections(request);
    };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::setDnsServers(const char *dnsServers, MegaRequestListener *listener)
{
    MegaRequestPrivate *request = new MegaRequestPrivate(MegaRequest::TYPE_RETRY_PENDING_CONNECTIONS, listener);
    request->setFlag(true);
    request->setNumber(true);
    request->setText(dnsServers);

    request->performRequest = [this, request]()
    {
        return performRequest_retryPendingConnections(request);
    };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::addEntropy(char *data, unsigned int size)
{
    if(client && client->rng.CanIncorporateEntropy())
    {
        client->rng.IncorporateEntropy((const byte*)data, size);
    }

#ifdef USE_OPENSSL
    RAND_seed(data, static_cast<int>(size));
#endif
}

string MegaApiImpl::userAttributeToString(int type)
{
    return User::attr2string((::mega::attr_t) type);
}

string MegaApiImpl::userAttributeToLongName(int type)
{
    return User::attr2longname((::mega::attr_t) type);
}

int MegaApiImpl::userAttributeFromString(const char *name)
{
    if (!name)
    {
        return MegaApi::USER_ATTR_UNKNOWN;
    }
    return User::string2attr(name);
}

char MegaApiImpl::userAttributeToScope(int type)
{
    char scope = User::scope(static_cast<attr_t>(type));
    if (scope == ATTR_SCOPE_UNKNOWN)
    {
        LOG_err << "Invalid scope for user attribute of type " << type;
    }

    return scope;
}

bool MegaApiImpl::serverSideRubbishBinAutopurgeEnabled()
{
    return client->ssrs_enabled;
}

bool MegaApiImpl::appleVoipPushEnabled()
{
    return client->aplvp_enabled;
}

bool MegaApiImpl::newLinkFormatEnabled()
{
    return client->mNewLinkFormat;
}

bool MegaApiImpl::accountIsNew() const
{
    return client->accountIsNew;
}

unsigned int MegaApiImpl::getABTestValue(const char* flag)
{
    if (!flag) return 0u;
    unique_ptr<uint32_t> v = client->mABTestFlags.get(flag);
    if (v)
    {
        sendABTestActive(flag, nullptr);
    }

    return v ? *v : 0u;
}

void MegaApiImpl::sendABTestActive(const char* flag, MegaRequestListener* listener)
{
    MegaRequestPrivate* request = new MegaRequestPrivate(MegaRequest::TYPE_AB_TEST_ACTIVE, listener);
    request->setText(flag);

    request->performRequest = [this, request]()
    {
        return client->sendABTestActive(request->getText(), [this, request](Error e)
        {
            fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e));
        });
    };

    requestQueue.push(request);
    waiter->notify();
}

int MegaApiImpl::smsAllowedState()
{
    return (client->mSmsVerificationState != SMS_STATE_UNKNOWN) ? client->mSmsVerificationState : 0;
}

char* MegaApiImpl::smsVerifiedPhoneNumber()
{
    SdkMutexGuard g(sdkMutex);
    return client->mSmsVerifiedPhone.empty() ? NULL : MegaApi::strdup(client->mSmsVerifiedPhone.c_str());
}

bool MegaApiImpl::multiFactorAuthAvailable()
{
    return client->gmfa_enabled;
}

void MegaApiImpl::multiFactorAuthEnable(const char *pin, MegaRequestListener *listener)
{
    return multiFactorAuthEnableOrDisable(pin, true, listener);
}

void MegaApiImpl::multiFactorAuthDisable(const char *pin, MegaRequestListener *listener)
{
    return multiFactorAuthEnableOrDisable(pin, false, listener);
}

void MegaApiImpl::multiFactorAuthLogin(const char *email, const char *password, const char *pin, MegaRequestListener *listener)
{
    MegaRequestPrivate *request = new MegaRequestPrivate(MegaRequest::TYPE_LOGIN, listener);
    request->setTransferredBytes(static_cast<long long>(cancel_epoch_snapshot()));
    request->setEmail(email);
    request->setPassword(password);
    request->setText(pin);

    request->performRequest = [this, request]()
    {
        return performRequest_login(request);
    };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::multiFactorAuthChangePassword(const char *oldPassword, const char *newPassword, const char *pin, MegaRequestListener *listener)
{
    MegaRequestPrivate *request = new MegaRequestPrivate(MegaRequest::TYPE_CHANGE_PW, listener);
    request->setPassword(oldPassword);
    request->setNewPassword(newPassword);
    request->setText(pin);

    request->performRequest = [this, request]()
    {
        return performRequest_changePw(request);
    };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::multiFactorAuthChangeEmail(const char *email, const char *pin, MegaRequestListener *listener)
{
    MegaRequestPrivate *request = new MegaRequestPrivate(MegaRequest::TYPE_GET_CHANGE_EMAIL_LINK, listener);
    request->setEmail(email);
    request->setText(pin);

    request->performRequest = [this, request]()
    {
        return performRequest_getChangeEmailLink(request);
    };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::multiFactorAuthCancelAccount(const char *pin, MegaRequestListener *listener)
{
    MegaRequestPrivate *request = new MegaRequestPrivate(MegaRequest::TYPE_GET_CANCEL_LINK, listener);
    request->setText(pin);

    request->performRequest = [this, request]()
    {
        return performRequest_getCancelLink(request);
    };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::fastLogin(const char *session, MegaRequestListener *listener)
{
    MegaRequestPrivate *request = new MegaRequestPrivate(MegaRequest::TYPE_LOGIN, listener);
    request->setTransferredBytes(static_cast<long long>(cancel_epoch_snapshot()));
    request->setSessionKey(session);

    request->performRequest = [this, request]()
    {
        return performRequest_login(request);
    };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::getUserData(MegaRequestListener *listener)
{
    MegaRequestPrivate *request = new MegaRequestPrivate(MegaRequest::TYPE_GET_USER_DATA, listener);

    request->performRequest = [this, request]()
    {
        return performRequest_getUserData(request);
    };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::getUserData(MegaUser *user, MegaRequestListener *listener)
{
    MegaRequestPrivate *request = new MegaRequestPrivate(MegaRequest::TYPE_GET_USER_DATA, listener);
    request->setFlag(true);
    if(user)
    {
        request->setEmail(user->getEmail());
    }

    request->performRequest = [this, request]()
    {
        return performRequest_getUserData(request);
    };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::getUserData(const char *user, MegaRequestListener *listener)
{
    MegaRequestPrivate *request = new MegaRequestPrivate(MegaRequest::TYPE_GET_USER_DATA, listener);
    request->setFlag(true);
    request->setEmail(user);

    request->performRequest = [this, request]()
    {
        return performRequest_getUserData(request);
    };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::login(const char *login, const char *password, MegaRequestListener *listener)
{
    MegaRequestPrivate *request = new MegaRequestPrivate(MegaRequest::TYPE_LOGIN, listener);
    request->setTransferredBytes(static_cast<long long>(cancel_epoch_snapshot()));
    request->setEmail(login);
    request->setPassword(password);

    request->performRequest = [this, request]()
    {
        return performRequest_login(request);
    };

    requestQueue.push(request);
    waiter->notify();
}

char *MegaApiImpl::dumpSession()
{
    SdkMutexGuard g(sdkMutex);
    string session;
    if (client->dumpsession(session))
    {
        return MegaApi::strdup(Base64::btoa(session).c_str());
    }
    return nullptr;
}

char *MegaApiImpl::getSequenceNumber()
{
    SdkMutexGuard g(sdkMutex);
    return MegaApi::strdup(client->scsn.text());
}

char *MegaApiImpl::getSequenceTag()
{
    SdkMutexGuard g(sdkMutex);

    //Note: we rely on mScDbStateRecord.seqTag, since mLastReceivedScSeqTag is cleared after notified
    return MegaApi::strdup(client->mScDbStateRecord.seqTag.c_str());
}

char *MegaApiImpl::getAccountAuth()
{
    SdkMutexGuard g(sdkMutex);
    if (client->loggedin())
    {
        return MegaApi::strdup(Base64::btoa(client->sid).c_str());
    }
    return nullptr;
}

void MegaApiImpl::setAccountAuth(const char *auth)
{
    SdkMutexGuard g(sdkMutex);

    client->setFolderLinkAccountAuth(auth);
}

void MegaApiImpl::createAccount(const char* email, const char* password, const char* firstname, const char* lastname, MegaHandle lastPublicHandle, int lastPublicHandleType, int64_t lastAccessTimestamp, MegaRequestListener *listener)
{
    MegaRequestPrivate *request = new MegaRequestPrivate(MegaRequest::TYPE_CREATE_ACCOUNT, listener);
    request->setEmail(email);
    request->setPassword(password);
    request->setName(firstname);
    request->setText(lastname);
    request->setNodeHandle(lastPublicHandle);
    request->setAccess(lastPublicHandleType);
    request->setTransferredBytes(lastAccessTimestamp);
    request->setParamType(MegaApi::CREATE_ACCOUNT);

    request->performRequest = [this, request]()
    {
        return performRequest_createAccount(request);
    };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::createEphemeralAccountPlusPlus(const char *firstname, const char *lastname, MegaRequestListener *listener)
{
    MegaRequestPrivate *request = new MegaRequestPrivate(MegaRequest::TYPE_CREATE_ACCOUNT, listener);
    request->setName(firstname);
    request->setText(lastname);
    request->setParamType(MegaApi::CREATE_EPLUSPLUS_ACCOUNT);

    request->performRequest = [this, request]()
    {
        return performRequest_createAccount(request);
    };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::resumeCreateAccount(const char *sid, MegaRequestListener *listener)
{
    MegaRequestPrivate *request = new MegaRequestPrivate(MegaRequest::TYPE_CREATE_ACCOUNT, listener);
    request->setSessionKey(sid);
    request->setParamType(MegaApi::RESUME_ACCOUNT);

    request->performRequest = [this, request]()
    {
        return performRequest_createAccount(request);
    };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::resumeCreateAccountEphemeralPlusPlus(const char *sid, MegaRequestListener *listener)
{
    MegaRequestPrivate *request = new MegaRequestPrivate(MegaRequest::TYPE_CREATE_ACCOUNT, listener);
    request->setSessionKey(sid);
    request->setParamType(MegaApi::RESUME_EPLUSPLUS_ACCOUNT);

    request->performRequest = [this, request]()
    {
        return performRequest_createAccount(request);
    };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::cancelCreateAccount(MegaRequestListener *listener)
{
    MegaRequestPrivate *request = new MegaRequestPrivate(MegaRequest::TYPE_CREATE_ACCOUNT, listener);
    request->setParamType(MegaApi::CANCEL_ACCOUNT);

    request->performRequest = [this, request]()
    {
        return performRequest_createAccount(request);
    };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::resendSignupLink(const char *email, const char *name, MegaRequestListener *listener)
{
    MegaRequestPrivate *request = new MegaRequestPrivate(MegaRequest::TYPE_SEND_SIGNUP_LINK, listener);
    request->setEmail(email);
    request->setName(name);

    request->performRequest = [this, request]()
    {
        return performRequest_sendSignupLink(request);
    };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::confirmAccount(const char* link, MegaRequestListener* listener)
{
    MegaRequestPrivate *request = new MegaRequestPrivate(MegaRequest::TYPE_CONFIRM_ACCOUNT, listener);
    request->setLink(link);

    request->performRequest = [this, request]()
    {
        return performRequest_confirmAccount(request);
    };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::cancelAccount(MegaRequestListener *listener)
{
    MegaRequestPrivate *request = new MegaRequestPrivate(MegaRequest::TYPE_GET_CANCEL_LINK, listener);

    request->performRequest = [this, request]()
    {
        return performRequest_getCancelLink(request);
    };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::changeEmail(const char *email, MegaRequestListener *listener)
{
    MegaRequestPrivate *request = new MegaRequestPrivate(MegaRequest::TYPE_GET_CHANGE_EMAIL_LINK, listener);
    request->setEmail(email);

    request->performRequest = [this, request]()
    {
        return performRequest_getChangeEmailLink(request);
    };

    requestQueue.push(request);
    waiter->notify();
}

MegaProxy *MegaApiImpl::getAutoProxySettings()
{
    MegaProxy *proxySettings = new MegaProxy;
    unique_ptr<Proxy> localProxySettings;

    {
        SdkMutexGuard g(sdkMutex);
        localProxySettings.reset(httpio->getautoproxy());
    }

    proxySettings->setProxyType(localProxySettings->getProxyType());
    if(localProxySettings->getProxyType() == Proxy::CUSTOM)
    {
        string localProxyURL = localProxySettings->getProxyURL();
        string proxyURL;
        LocalPath::local2path(&localProxyURL, &proxyURL, true);
        LOG_debug << "Autodetected proxy: " << proxyURL;
        proxySettings->setProxyURL(proxyURL.c_str());
    }

    return proxySettings;
}

void MegaApiImpl::loop()
{
#ifdef _WIN32
    httpio->lock();
#endif

    while(true)
    {
        int r;
        {
            SdkMutexGuard g(sdkMutex);
            r = client->preparewait();
        }

        if (!r)
        {
            r = client->dowait();

            {
                SdkMutexGuard g(sdkMutex);
                r |= client->checkevents();
            }
        }

        if (r & Waiter::NEEDEXEC)
        {
            WAIT_CLASS::bumpds();
            updateBackups();
            if (sendPendingTransfers(nullptr))
            {
                yield();
            }
            sendPendingRequests();
            sendPendingScRequest();
            if (threadExit)
            {
                break;
            }

            {
                SdkMutexGuard g(sdkMutex);
                client->exec();
            }
        }
    }

    SdkMutexGuard g(sdkMutex);
    delete client;
    client = nullptr;
}

bool MegaApiImpl::createLocalFolder(const char *path)
{
    if (!path)
    {
        return false;
    }

    string sPath(path);

    auto localpath = LocalPath::fromAbsolutePath(sPath);

    SdkMutexGuard g(sdkMutex);
    return client->fsaccess->mkdirlocal(localpath, false, true);
}

Error MegaApiImpl::createLocalFolder_unlocked(LocalPath& localPath,
                                              FileSystemAccess& fsaccess,
                                              const CollisionResolution& collisionResolution)
{
    auto da = fsaccess.newfileaccess();
    LocalPath currentLeafName;
    // Try to open the target path in case-sensitive mode
    // (unless collision resolution is Overwrite, in which case open insensitive case (merge
    // folders))
    bool opened = da->fopen(localPath,
                            OPEN_RDONLY,
                            FSLogging::logOnError,
                            nullptr,
                            false,
                            (collisionResolution == CollisionResolution::Overwrite) ? true : false,
                            &currentLeafName);
    if (!opened)
    {
        // Target folder doesn't exist, try to create it
        bool mkdirSucceeded = fsaccess.mkdirlocal(localPath, false, false);
        if (!mkdirSucceeded)
        {
            // If the folder still doesn't exist, report an error
            if (!fsaccess.target_exists)
            {
                LOG_err << "Unable to create folder: " << localPath;
                return API_EWRITE;
            }

            // Target already exists — apply collision resolution strategy
            switch (collisionResolution)
            {
                case CollisionResolution::RenameExistingToOldN:
                {
                    auto newPath = FileNameGenerator::suffixWithOldN(da.get(), localPath);
                    fsaccess.renamelocal(localPath, newPath, true);
                    break;
                }
                case CollisionResolution::RenameNewWithN:
                    localPath = FileNameGenerator::suffixWithN(da.get(), localPath);
                    break;
                case CollisionResolution::Overwrite:
                    LOG_err << "Unexpected error for Overwrite CollisionResolution " << localPath;
                    return API_EINTERNAL;
                    break;
                default:
                    LOG_err << "Invalid folder resolution strategy " << localPath;
                    return API_EARGS;
                    break;
            }

            // Retry folder creation with the new (renamed) path
            if (!fsaccess.mkdirlocal(localPath, false, false))
            {
                LOG_err << "Unable to create folder: " << localPath;
                return API_EWRITE;
            }
        }
    }
    else if (da->type == FILENODE)
    {
        LOG_err << "Local file detected where there should be a folder: " << localPath;
        return API_EARGS;
    }
    else
    {
        LOG_debug << "Already existing folder detected: " << localPath;
        return API_EEXIST;
    }
    return API_OK;
}

void MegaApiImpl::moveNode(MegaNode *node, MegaNode *newParent, MegaRequestListener *listener)
{
    moveNode(node, newParent, nullptr, listener);
}

void MegaApiImpl::copyNode(MegaNode *node, MegaNode* target, MegaRequestListener *listener)
{
    MegaRequestPrivate *request = new MegaRequestPrivate(MegaRequest::TYPE_COPY, listener);
    if (node)
    {
        request->setPublicNode(node, true);
        request->setNodeHandle(node->getHandle());
    }
    if(target) request->setParentHandle(target->getHandle());

    request->performRequest = [this, request]()
    {
        return performRequest_copy(request);
    };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::copyNode(MegaNode *node, MegaNode *target, const char *newName, MegaRequestListener *listener)
{
    MegaRequestPrivate *request = new MegaRequestPrivate(MegaRequest::TYPE_COPY, listener);
    if (node)
    {
        request->setPublicNode(node, true);
        request->setNodeHandle(node->getHandle());
    }
    if(target) request->setParentHandle(target->getHandle());
    request->setName(newName);

    request->performRequest = [this, request]()
    {
        return performRequest_copy(request);
    };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::sendFileToUser(MegaNode *node, MegaUser *user, MegaRequestListener *listener)
{
    return sendFileToUser(node, user ? user->getEmail() : NULL, listener);
}

void MegaApiImpl::sendFileToUser(MegaNode *node, const char* email, MegaRequestListener *listener)
{
    MegaRequestPrivate *request = new MegaRequestPrivate(MegaRequest::TYPE_COPY, listener);
    if (node)
    {
        request->setPublicNode(node, true);
        request->setNodeHandle(node->getHandle());
    }
    request->setEmail(email);

    request->performRequest = [this, request]()
    {
        return performRequest_copy(request);
    };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::upgradeSecurity(MegaRequestListener* listener)
{
    MegaRequestPrivate *request = new MegaRequestPrivate(MegaRequest::TYPE_UPGRADE_SECURITY, listener);
    request->performRequest = [this, request]()
    {
        client->upgradeSecurity([this, request](Error e) {
            fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e));
        });

        return API_OK;
    };

    requestQueue.push(request);
    waiter->notify();
}

bool MegaApiImpl::contactVerificationWarningEnabled()
{
    SdkMutexGuard m(sdkMutex);
    return client->mKeyManager.getContactVerificationWarning();
}

void MegaApiImpl::setManualVerificationFlag(bool enable)
{
    SdkMutexGuard m(sdkMutex);
    client->mKeyManager.setManualVerificationFlag(enable);
}

void MegaApiImpl::openShareDialog(MegaNode* node, MegaRequestListener* listener)
{
    MegaRequestPrivate *request = new MegaRequestPrivate(MegaRequest::TYPE_OPEN_SHARE_DIALOG, listener);
    if (node)
    {
        request->setNodeHandle(node->getHandle());
    }

    request->performRequest = [this, request]()
    {
        shared_ptr<Node> node = client->nodebyhandle(request->getNodeHandle());

        client->openShareDialog(node.get(), [this, request](Error e) {
            fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e));
        });

        return API_OK;
    };

    requestQueue.push(request);
    waiter->notify();
}

MegaShareList *MegaApiImpl::getUnverifiedInShares(int order)
{
    SdkMutexGuard lock(sdkMutex);

    sharedNode_vector nodes = client->getUnverifiedInShares();

    sortByComparatorFunction(nodes, order, *client);

    vector<impl::ShareData> shares;
    for (const auto& node: nodes)
    {
        shares.emplace_back(node->nodehandle, node->inshare.get(), false);
    }

    return new MegaShareListPrivate(shares);
}

MegaShareList *MegaApiImpl::getUnverifiedOutShares(int order)
{
    SdkMutexGuard guard(sdkMutex);

    // Get shared nodes
    sharedNode_vector sharedNodes = getSharedNodes();

    // Sort in place
    MegaApiImpl::sortByComparatorFunction(sharedNodes, order, *client);

    // Predicate if a share is unverified or not
    auto isUnverified = [](const impl::ShareData& data)
    {
        return !data.isVerified();
    };

    // Extract unverified shares
    auto unverifiedShares =
        impl::ShareExtractor::extractOutShares(sharedNodes, client->mKeyManager, isUnverified);

    // Sort shares in place
    impl::ShareSorter::sort(unverifiedShares, order);

    return new MegaShareListPrivate(unverifiedShares);
}

void MegaApiImpl::share(MegaNode* node, MegaUser *user, int access, MegaRequestListener *listener)
{
    return share(node, user ? user->getEmail() : NULL, access, listener);
}

void MegaApiImpl::loginToFolder(const char* megaFolderLink,
                                const char* authKey,
                                bool tryToResumeFolderLinkFromCache,
                                MegaRequestListener* listener)
{
    MegaRequestPrivate *request = new MegaRequestPrivate(MegaRequest::TYPE_LOGIN, listener);
    request->setTransferredBytes(static_cast<long long>(cancel_epoch_snapshot()));
    request->setLink(megaFolderLink);
    request->setPassword(authKey);
    request->setEmail("FOLDER");
    request->setFlag(tryToResumeFolderLinkFromCache);

    request->performRequest = [this, request]()
    {
        return performRequest_login(request);
    };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::importFileLink(const char* megaFileLink, MegaNode *parent, MegaRequestListener *listener)
{
    MegaRequestPrivate *request = new MegaRequestPrivate(MegaRequest::TYPE_IMPORT_LINK, listener);
    if(parent) request->setParentHandle(parent->getHandle());
    request->setLink(megaFileLink);

    request->performRequest = [this, request]()
    {
        return performRequest_importLink_getPublicNode(request);
    };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::decryptPasswordProtectedLink(const char *link, const char *password, MegaRequestListener *listener)
{
    MegaRequestPrivate *request = new MegaRequestPrivate(MegaRequest::TYPE_PASSWORD_LINK, listener);
    request->setLink(link);
    request->setPassword(password);

    request->performRequest = [this, request]()
    {
        return performRequest_passwordLink(request);
    };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::encryptLinkWithPassword(const char *link, const char *password, MegaRequestListener *listener)
{
    MegaRequestPrivate *request = new MegaRequestPrivate(MegaRequest::TYPE_PASSWORD_LINK, listener);
    request->setLink(link);
    request->setPassword(password);
    request->setFlag(true); // encrypt

    request->performRequest = [this, request]()
    {
        return performRequest_passwordLink(request);
    };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::getPublicNode(const char* megaFileLink, MegaRequestListener *listener)
{
    MegaRequestPrivate *request = new MegaRequestPrivate(MegaRequest::TYPE_GET_PUBLIC_NODE, listener);
    request->setLink(megaFileLink);

    request->performRequest = [this, request]()
    {
        return performRequest_importLink_getPublicNode(request);
    };

    requestQueue.push(request);
    waiter->notify();
}

const char *MegaApiImpl::buildPublicLink(const char *publicHandle, const char *key, bool isFolder)
{
    handle ph = MegaApi::base64ToHandle(publicHandle);
    string link = client->publicLinkURL(client->mNewLinkFormat, isFolder ? TypeOfLink::FOLDER : TypeOfLink::FILE, ph, key);
    return MegaApi::strdup(link.c_str());
}

void MegaApiImpl::getThumbnail(MegaNode* node, const char *dstFilePath, MegaRequestListener *listener)
{
    getNodeAttribute(node, GfxProc::THUMBNAIL, dstFilePath, listener);
}

void MegaApiImpl::getThumbnail(MegaHandle handle,
                               const char* dstFilePath,
                               MegaRequestListener* listener)
{
    getNodeAttribute(handle, GfxProc::THUMBNAIL, dstFilePath, listener);
}

void MegaApiImpl::cancelGetThumbnail(MegaNode* node, MegaRequestListener *listener)
{
    cancelGetNodeAttribute(node, GfxProc::THUMBNAIL, listener);
}

void MegaApiImpl::setThumbnail(MegaNode* node, const char *srcFilePath, MegaRequestListener *listener)
{
    setNodeAttribute(node, GfxProc::THUMBNAIL, srcFilePath, INVALID_HANDLE, listener);
}

void MegaApiImpl::putThumbnail(MegaBackgroundMediaUpload* bu, const char *srcFilePath, MegaRequestListener *listener)
{
    putNodeAttribute(bu, GfxProc::THUMBNAIL, srcFilePath, listener);
}

void MegaApiImpl::setThumbnailByHandle(MegaNode* node, MegaHandle attributehandle, MegaRequestListener *listener)
{
    setNodeAttribute(node, GfxProc::THUMBNAIL, nullptr, attributehandle, listener);
}

void MegaApiImpl::getPreview(MegaNode* node, const char *dstFilePath, MegaRequestListener *listener)
{
    getNodeAttribute(node, GfxProc::PREVIEW, dstFilePath, listener);
}

void MegaApiImpl::cancelGetPreview(MegaNode* node, MegaRequestListener *listener)
{
    cancelGetNodeAttribute(node, GfxProc::PREVIEW, listener);
}

void MegaApiImpl::setPreview(MegaNode* node, const char *srcFilePath, MegaRequestListener *listener)
{
    setNodeAttribute(node, GfxProc::PREVIEW, srcFilePath, INVALID_HANDLE, listener);
}

void MegaApiImpl::putPreview(MegaBackgroundMediaUpload* bu, const char *srcFilePath, MegaRequestListener *listener)
{
    putNodeAttribute(bu, GfxProc::PREVIEW, srcFilePath, listener);
}

void MegaApiImpl::setPreviewByHandle(MegaNode* node, MegaHandle attributehandle, MegaRequestListener *listener)
{
    setNodeAttribute(node, GfxProc::PREVIEW, nullptr, attributehandle, listener);
}

void MegaApiImpl::getUserAvatar(MegaUser* user, const char *dstFilePath, MegaRequestListener *listener)
{
    const char *email = NULL;
    if (user)
    {
        email = user->getEmail();
    }
    getUserAttr(email, MegaApi::USER_ATTR_AVATAR, dstFilePath, 0, listener);
}

void MegaApiImpl::getUserAvatar(const char* email_or_handle, const char *dstFilePath, MegaRequestListener *listener)
{
    getUserAttr(email_or_handle, MegaApi::USER_ATTR_AVATAR, dstFilePath, 0, listener);
}

char *MegaApiImpl::getUserAvatarColor(MegaUser *user)
{
    return user ? MegaApiImpl::getAvatarColor((handle) user->getHandle()) : NULL;
}

char *MegaApiImpl::getUserAvatarColor(const char *userhandle)
{
    return userhandle ? MegaApiImpl::getAvatarColor(MegaApiImpl::base64ToUserHandle(userhandle)) : NULL;
}

char *MegaApiImpl::getUserAvatarSecondaryColor(MegaUser *user)
{
    return user ? MegaApiImpl::getAvatarSecondaryColor((handle) user->getHandle()) : NULL;
}

char *MegaApiImpl::getUserAvatarSecondaryColor(const char *userhandle)
{
    return userhandle ? MegaApiImpl::getAvatarSecondaryColor(MegaApiImpl::base64ToUserHandle(userhandle)) : NULL;
}

void MegaApiImpl::setAvatar(const char *dstFilePath, MegaRequestListener *listener)
{
    setUserAttr(MegaApi::USER_ATTR_AVATAR, dstFilePath, listener);
}

char* MegaApiImpl::getPrivateKey(int type)
{
    SdkMutexGuard g(sdkMutex);

    if (type < MegaApi::PRIVATE_KEY_ED25519 || type > MegaApi::PRIVATE_KEY_CU25519)
    {
        return nullptr;
    }

    User *u = client->ownuser();
    if (!u)
    {
        LOG_warn << "User is not defined yet";
        assert(false);
        return nullptr;
    }

    string privateKey;
    if (client->mKeyManager.generation())   // account has ^!keys already available
    {
        switch (type)
        {
        case MegaApi::PRIVATE_KEY_ED25519:
            privateKey = client->mKeyManager.privEd25519();
            break;
        case MegaApi::PRIVATE_KEY_CU25519:
            privateKey = client->mKeyManager.privCu25519();
            break;
        default:
            assert(false);
            return nullptr;
        }
    }
    else
    {
        const UserAttribute* attribute = u->getAttribute(ATTR_KEYRING);
        if (attribute && attribute->isValid())
        {
            unique_ptr<string_map> records{
                tlv::containerToRecords(attribute->value(), client->key)};
            if (records &&
                (type == MegaApi::PRIVATE_KEY_ED25519 || type == MegaApi::PRIVATE_KEY_CU25519))
            {
                privateKey = type == MegaApi::PRIVATE_KEY_ED25519 ?
                                 std::move((*records)[EdDSA::TLV_KEY]) :
                                 std::move((*records)[ECDH::TLV_KEY]);
            }
            else
            {
                LOG_warn << "Failed to decrypt keyring while initialization or invalid key type";
                return nullptr;
            }
        }
        else
        {
            return nullptr;
        }
    }


    std::string privateKeyBase64 = Base64::btoa(privateKey);

    return MegaApi::strdup(privateKeyBase64.c_str());
}

void MegaApiImpl::getUserAttribute(MegaUser* user, int type, MegaRequestListener *listener)
{
    const char* email = NULL;
    if (user)
    {
        email = user->getEmail();
    }
    getUserAttribute(email, type, listener);
}

bool MegaApiImpl::testAllocation(unsigned allocCount, size_t allocSize)
{
    bool success = true;
    std::vector<char*> v;
    try
    {
        for (unsigned i = allocCount; i--; )
        {
            v.push_back(new char[allocSize]);
        }
    }
    catch (std::bad_alloc&)
    {
        LOG_warn << "MegaApi::testAllocation detected low memory: " << allocCount << " " << allocSize;
        success = false;
    }
    for (auto it : v)
    {
        delete[] it;
    }
    return success;
}

void MegaApiImpl::getUserAttribute(const char* email_or_handle, int type, MegaRequestListener *listener)
{
    // allow only types documented to be valid
    switch (type)
    {
        case ATTR_FIRSTNAME:
        case ATTR_LASTNAME:
        case ATTR_AUTHRING:
        case ATTR_LAST_INT:
        case ATTR_ED25519_PUBK:
        case ATTR_CU25519_PUBK:
        case ATTR_KEYRING:
        case ATTR_SIG_RSA_PUBK:
        case ATTR_SIG_CU255_PUBK:
        case ATTR_LANGUAGE:
        case ATTR_PWD_REMINDER:
        case ATTR_DISABLE_VERSIONS:
        case ATTR_CONTACT_LINK_VERIFICATION:
        case ATTR_RICH_PREVIEWS:
        case ATTR_RUBBISH_TIME:
        case ATTR_LAST_PSA:
        case ATTR_STORAGE_STATE:
        case ATTR_GEOLOCATION:
        case ATTR_CAMERA_UPLOADS_FOLDER:
        case ATTR_MY_CHAT_FILES_FOLDER:
        case ATTR_PUSH_SETTINGS:
        case ATTR_ALIAS:
        case ATTR_DEVICE_NAMES:
        case ATTR_MY_BACKUPS_FOLDER:
        case ATTR_COOKIE_SETTINGS:
        case ATTR_JSON_SYNC_CONFIG_DATA:
        case ATTR_NO_CALLKIT:
        case ATTR_APPS_PREFS:
        case ATTR_CC_PREFS:
        case ATTR_VISIBLE_WELCOME_DIALOG:
        case ATTR_VISIBLE_TERMS_OF_SERVICE:
        case ATTR_PWM_BASE:
        case ATTR_LAST_READ_NOTIFICATION:
        case ATTR_LAST_ACTIONED_BANNER:
        case ATTR_RECENT_CLEAR_TIMESTAMP:
        case ATTR_LAST_PURGE_ACKNOWLEDGED:
        // undocumented types, allowed only for testing:
        case ATTR_KEYS:
        case ATTR_DEV_OPT:
            getUserAttr(email_or_handle, type, nullptr, 0, listener);
            break;
        default:
            getUserAttr(email_or_handle, ATTR_UNKNOWN, nullptr, 0, listener);
    }
}

void MegaApiImpl::getChatUserAttribute(const char *email_or_handle, int type, const char *ph, MegaRequestListener *listener)
{
    getChatUserAttr(email_or_handle, type ? type : -1, NULL, ph, 0, listener);
}

void MegaApiImpl::setUserAttribute(int type, const char *value, MegaRequestListener *listener)
{
    // allow only types documented to be valid
    switch (type)
    {
        case ATTR_FIRSTNAME:
        case ATTR_LASTNAME:
        case ATTR_LANGUAGE:
        case ATTR_DISABLE_VERSIONS:
        case ATTR_CONTACT_LINK_VERIFICATION:
        case ATTR_RUBBISH_TIME:
        case ATTR_LAST_PSA:
        case ATTR_PUSH_SETTINGS:
        case ATTR_NO_CALLKIT:
        case ATTR_VISIBLE_WELCOME_DIALOG:
        case ATTR_VISIBLE_TERMS_OF_SERVICE:
        case ATTR_LAST_READ_NOTIFICATION:
        case ATTR_LAST_ACTIONED_BANNER:
        case ATTR_LAST_PURGE_ACKNOWLEDGED:
        // undocumented types, allowed only for testing:
        case ATTR_ENABLE_TEST_NOTIFICATIONS:
        case ATTR_ENABLE_TEST_SURVEYS:
        // undocumented types, allowed only to notify with a specific error:
        case ATTR_KEYRING:
        case ATTR_KEYS:
        case ATTR_AUTHRING:
        case ATTR_AUTHCU255:
        case ATTR_CU25519_PUBK:
        case ATTR_ED25519_PUBK:
        case ATTR_SIG_CU255_PUBK:
        case ATTR_SIG_RSA_PUBK:
        case ATTR_PWD_REMINDER:
        case ATTR_MY_BACKUPS_FOLDER:
        case ATTR_DEV_OPT:
            setUserAttr(type, value, listener);
            break;
        default:
            setUserAttr(ATTR_UNKNOWN, value, listener);
    }
}

void MegaApiImpl::setUserAttribute(int type,
                                   const MegaStringMap* value,
                                   MegaRequestListener* listener)
{
    // allow only types documented to be valid
    switch (type)
    {
        case ATTR_AUTHRING:
        case ATTR_LAST_INT:
        case ATTR_KEYRING:
        case ATTR_RICH_PREVIEWS:
        case ATTR_GEOLOCATION:
        case ATTR_ALIAS:
        case ATTR_DEVICE_NAMES:
        case ATTR_APPS_PREFS:
        case ATTR_CC_PREFS:
            setUserAttr(type, value, listener);
            break;
        default:
            setUserAttr(ATTR_UNKNOWN, value, listener);
    }
}

void MegaApiImpl::setUserAttr(int type, const MegaStringMap* value, MegaRequestListener* listener)
{
    MegaRequestPrivate *request = new MegaRequestPrivate(MegaRequest::TYPE_SET_ATTR_USER, listener);

    request->setMegaStringMap(value);
    request->setParamType(type);

    request->performRequest = [this, request]()
    {
        return performRequest_setAttrUser(request);
    };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::getRubbishBinAutopurgePeriod(MegaRequestListener *listener)
{
    MegaRequestPrivate *request = new MegaRequestPrivate(MegaRequest::TYPE_GET_ATTR_USER, listener);
    request->setParamType(MegaApi::USER_ATTR_RUBBISH_TIME);

    request->performRequest = [this, request]()
    {
        return performRequest_getAttrUser(request);
    };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::setRubbishBinAutopurgePeriod(int days, MegaRequestListener *listener)
{
    ostringstream oss;
    oss << days;
    string value = oss.str();
    MegaRequestPrivate *request = new MegaRequestPrivate(MegaRequest::TYPE_SET_ATTR_USER, listener);
    request->setText(value.data());
    request->setParamType(MegaApi::USER_ATTR_RUBBISH_TIME);
    request->setNumber(days);

    request->performRequest = [this, request]()
    {
        return performRequest_setAttrUser(request);
    };

    requestQueue.push(request);
    waiter->notify();
}

const char* MegaApiImpl::getDeviceId() const
{
    return MegaApi::strdup(client->getDeviceidHash().c_str());
}

void MegaApiImpl::getDeviceName(const char* deviceId, MegaRequestListener *listener)
{
    MegaRequestPrivate *request = new MegaRequestPrivate(MegaRequest::TYPE_GET_ATTR_USER, listener);
    request->setParamType(MegaApi::USER_ATTR_DEVICE_NAMES);
    string id = deviceId ? deviceId : client->getDeviceidHash();
    request->setText(id.c_str());

    request->performRequest = [this, request]()
    {
        return performRequest_getAttrUser(request);
    };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::setDeviceName(const char *deviceId, const char *deviceName, MegaRequestListener *listener)
{
    MegaRequestPrivate *request = new MegaRequestPrivate(MegaRequest::TYPE_SET_ATTR_USER, listener);
    MegaStringMapPrivate stringMap;
    string id = deviceId ? deviceId : client->getDeviceidHash();
    string buf = deviceName ? deviceName : "";
    stringMap.set(id.c_str(), Base64::btoa(buf).c_str());
    request->setMegaStringMap(&stringMap);
    request->setText(id.c_str());
    request->setName(deviceName);
    request->setParamType(MegaApi::USER_ATTR_DEVICE_NAMES);

    request->performRequest = [this, request]()
    {
        return performRequest_setAttrUser(request);
    };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::getDriveName(const char *pathToDrive, MegaRequestListener *listener)
{
    MegaRequestPrivate *request = new MegaRequestPrivate(MegaRequest::TYPE_GET_ATTR_USER, listener);
    request->setParamType(MegaApi::USER_ATTR_DEVICE_NAMES);
    request->setFlag(true); // external drive
    request->setFile(pathToDrive);

    request->performRequest = [this, request]()
    {
        return performRequest_getAttrUser(request);
    };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::setDriveName(const char *pathToDrive, const char *driveName, MegaRequestListener *listener)
{
    MegaRequestPrivate *request = new MegaRequestPrivate(MegaRequest::TYPE_SET_ATTR_USER, listener);
    request->setFile(pathToDrive);
    request->setName(driveName);
    request->setParamType(MegaApi::USER_ATTR_DEVICE_NAMES);
    request->setFlag(true); // external drive

    request->performRequest = [this, request]()
    {
        return performRequest_setAttrUser(request);
    };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::setCustomNodeAttribute(MegaNode *node, const char *attrName, const char *value, MegaRequestListener *listener)
{
    MegaRequestPrivate *request = new MegaRequestPrivate(MegaRequest::TYPE_SET_ATTR_NODE, listener);
    if(node) request->setNodeHandle(node->getHandle());
    request->setName(attrName);
    request->setText(value);
    request->setFlag(false);     // is official attribute or not

    request->performRequest = [this, request]()
    {
        return performRequest_setAttrNode(request);
    };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::setNodeS4(MegaNode *node, const char *value, MegaRequestListener *listener)
{
    MegaRequestPrivate *request = new MegaRequestPrivate(MegaRequest::TYPE_SET_ATTR_NODE, listener);
    if(node) request->setNodeHandle(node->getHandle());
    request->setParamType(MegaApi::NODE_ATTR_S4);
    request->setText(value);
    request->setFlag(true);     // is official attribute or not

    request->performRequest = [this, request]()
    {
        return performRequest_setAttrNode(request);
    };

    requestQueue.push(request);
    waiter->notify();
}

bool MegaApiImpl::isS4Enabled()
{
    return client->mIsS4Enabled.load();
}

MegaHandle MegaApiImpl::getS4Container()
{
    return client->mS4Container.load().as8byte();
}

void MegaApiImpl::setNodeLabel(MegaNode *node, int label, MegaRequestListener *listener)
{
    MegaRequestPrivate *request = new MegaRequestPrivate(MegaRequest::TYPE_SET_ATTR_NODE, listener);
    if(node) request->setNodeHandle(node->getHandle());
    request->setParamType(MegaApi::NODE_ATTR_LABEL);
    request->setNumDetails(label);
    request->setFlag(true);     // is official attribute or not

    request->performRequest = [this, request]()
    {
        return performRequest_setAttrNode(request);
    };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::setNodeFavourite(MegaNode *node, bool fav, MegaRequestListener *listener)
{
    MegaRequestPrivate *request = new MegaRequestPrivate(MegaRequest::TYPE_SET_ATTR_NODE, listener);
    if(node) request->setNodeHandle(node->getHandle());
    request->setParamType(MegaApi::NODE_ATTR_FAV);
    request->setNumDetails(fav);
    request->setFlag(true);     // is official attribute or not

    request->performRequest = [this, request]()
    {
        return performRequest_setAttrNode(request);
    };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::setNodeSensitive(MegaNode* node, bool sensitive, MegaRequestListener* listener)
{
    MegaRequestPrivate* request = new MegaRequestPrivate(MegaRequest::TYPE_SET_ATTR_NODE, listener);
    if (node) request->setNodeHandle(node->getHandle());
    request->setParamType(MegaApi::NODE_ATTR_SEN);
    request->setNumDetails(sensitive);
    request->setFlag(true);     // is official attribute or not

    request->performRequest = [this, request]()
    {
        return performRequest_setAttrNode(request);
    };

    requestQueue.push(request);
    waiter->notify();
}

bool MegaApiImpl::isSensitiveInherited(MegaNode* mnode)
{
    // there is no MegaNode::getParentNode() so the traversal must be here in MegaApi

    SdkMutexGuard g(sdkMutex);

    std::shared_ptr<Node> node = client->nodeByHandle(NodeHandle().set6byte(mnode->getHandle()));
    if (node == nullptr)
        return false;
    return node->isSensitiveInherited();
}

static void encodeCoordinates(double latitude, double longitude, int& lat, int& lon)
{
    lat = int(latitude);
    if (latitude != MegaNode::INVALID_COORDINATE)
    {
        lat = int(((latitude + 90) / 180) * 0xFFFFFF);
    }

    lon = int(longitude);
    if (longitude != MegaNode::INVALID_COORDINATE)
    {
        lon = int((longitude == 180) ? 0 : ((longitude + 180) / 360) * 0x01000000);
    }
}

void MegaApiImpl::setNodeCoordinates(std::variant<MegaNode*, MegaHandle> nodeOrNodeHandle,
                                     bool unshareable,
                                     double latitude,
                                     double longitude,
                                     MegaRequestListener* listener)
{
    MegaRequestPrivate *request = new MegaRequestPrivate(MegaRequest::TYPE_SET_ATTR_NODE, listener);

    MegaHandle nodeHandle{INVALID_HANDLE};
    if (std::holds_alternative<MegaNode*>(nodeOrNodeHandle))
    {
        if (auto node{std::get<MegaNode*>(nodeOrNodeHandle)})
        {
            nodeHandle = node->getHandle();
        }
    }
    else if (std::holds_alternative<MegaHandle>(nodeOrNodeHandle))
    {
        nodeHandle = std::get<MegaHandle>(nodeOrNodeHandle);
    }
    request->setNodeHandle(nodeHandle);

    int lat, lon;
    encodeCoordinates(latitude, longitude, lat, lon);

    request->setParamType(MegaApi::NODE_ATTR_COORDINATES);
    request->setTransferTag(lat);
    request->setNumDetails(lon);
    request->setAccess(unshareable);
    request->setFlag(true);     // official attribute (otherwise it would go in the custom section)

    request->performRequest = [this, request]()
    {
        return performRequest_setAttrNode(request);
    };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::setNodeDescription(MegaNode* node,
                                     const char* description,
                                     MegaRequestListener* listener)
{
    MegaRequestPrivate* request = new MegaRequestPrivate(MegaRequest::TYPE_SET_ATTR_NODE, listener);

    if (node)
    {
        request->setNodeHandle(node->getHandle());
    }

    request->setParamType(MegaApi::NODE_ATTR_DESCRIPTION);
    request->setText(description);
    request->setFlag(true); // official attribute (otherwise it would go in the custom section)

    request->performRequest = [this, request]()
    {
        return performRequest_setAttrNode(request);
    };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::addNodeTag(MegaNode* node, const char* tag, MegaRequestListener* listener)
{
    CRUDNodeTagOperation(node, MegaApi::TAG_NODE_SET, tag, nullptr, listener);
}

void MegaApiImpl::removeNodeTag(MegaNode* node, const char* tag, MegaRequestListener* listener)
{
    CRUDNodeTagOperation(node, MegaApi::TAG_NODE_REMOVE, tag, nullptr, listener);
}

void MegaApiImpl::updateNodeTag(MegaNode* node, const char* newTag, const char* oldTag, MegaRequestListener* listener)
{
    CRUDNodeTagOperation(node, MegaApi::TAG_NODE_UPDATE, newTag, oldTag, listener);
}

MegaStringList* MegaApiImpl::getAllNodeTagsBelow(MegaHandle handle,
                                                 const std::string& pattern,
                                                 CancelToken cancelToken)
{
    // Try and retrieve all tags below this account's root nodes.
    auto tags = ([&]() {
        // Make sure we have exclusive access to the client.
        SdkMutexGuard guard(sdkMutex);

        // Ask the client for the list of tags.
        return client->getNodeTagsBelow(std::move(cancelToken),
                                        NodeHandle().set6byte(handle),
                                        pattern);
    })();

    // Couldn't get a list of tags.
    if (!tags)
        return new MegaStringListPrivate();

    // Make sure the tags are in sorted order.
    std::vector<std::string> sorted(tags->begin(), tags->end());
    std::sort(sorted.begin(), sorted.end(), NaturalSortingComparator());

    // Return sorted list of tags to the caller.
    return new MegaStringListPrivate(std::move(sorted));
}

void MegaApiImpl::exportNode(MegaNode *node, int64_t expireTime, bool writable, bool megaHosted, MegaRequestListener *listener)
{
    MegaRequestPrivate *request = new MegaRequestPrivate(MegaRequest::TYPE_EXPORT, listener);
    if(node) request->setNodeHandle(node->getHandle());
    request->setNumber(expireTime);
    request->setAccess(1);
    request->setTransferTag(megaHosted ? 1 : 0);
    request->setFlag(writable);

    request->performRequest = [this, request]()
    {
        return performRequest_export(request);
    };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::disableExport(MegaNode *node, MegaRequestListener *listener)
{
    MegaRequestPrivate *request = new MegaRequestPrivate(MegaRequest::TYPE_EXPORT, listener);
    if(node) request->setNodeHandle(node->getHandle());
    request->setAccess(0);

    request->performRequest = [this, request]()
    {
        return performRequest_export(request);
    };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::getPricing(const std::optional<std::string>& countryCode,
                             MegaRequestListener* listener)
{
    MegaRequestPrivate *request = new MegaRequestPrivate(MegaRequest::TYPE_GET_PRICING, listener);
    if (countryCode && countryCode->size())
    {
        request->setText(countryCode->c_str());
    }

    request->performRequest = [this, request]()
    {
        return performRequest_enumeratequotaitems(request);
    };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::getRecommendedProLevel(MegaRequestListener* listener)
{
    MegaRequestPrivate* request = new MegaRequestPrivate(MegaRequest::TYPE_GET_RECOMMENDED_PRO_PLAN, listener);

    request->performRequest = [this, request]() {
        if (client->loggedin() != FULLACCOUNT)
        {
            return API_EACCESS;
        }

        client->getaccountdetails(request->getAccountDetails(), true /*storage*/, false, false, false, false, false, -1);
        return API_OK;
    };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::getPaymentId(handle productHandle, handle lastPublicHandle, int lastPublicHandleType, int64_t lastAccessTimestamp, MegaRequestListener *listener)
{
    MegaRequestPrivate *request = new MegaRequestPrivate(MegaRequest::TYPE_GET_PAYMENT_ID, listener);
    request->setNodeHandle(productHandle);
    request->setParentHandle(lastPublicHandle);
    request->setParamType(lastPublicHandleType);
    request->setTransferredBytes(lastAccessTimestamp);

    request->performRequest = [this, request]()
    {
        return performRequest_enumeratequotaitems(request);
    };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::upgradeAccount(MegaHandle productHandle, int paymentMethod, MegaRequestListener *listener)
{
    MegaRequestPrivate *request = new MegaRequestPrivate(MegaRequest::TYPE_UPGRADE_ACCOUNT, listener);
    request->setNodeHandle(productHandle);
    request->setNumber(paymentMethod);

    request->performRequest = [this, request]()
    {
        return performRequest_enumeratequotaitems(request);
    };

    requestQueue.push(request);
    waiter->notify();
}

char *MegaApiImpl::exportMasterKey()
{
    SdkMutexGuard g(sdkMutex);
    char* buf = NULL;

    if(client->loggedin())
    {
        buf = new char[SymmCipher::KEYLENGTH * 4 / 3 + 4];
        Base64::btoa(client->key.key, SymmCipher::KEYLENGTH, buf);
    }

    return buf;
}

void MegaApiImpl::updatePwdReminderData(bool lastSuccess, bool lastSkipped, bool mkExported, bool dontShowAgain, bool lastLogin, MegaRequestListener *listener)
{
    MegaRequestPrivate *request = new MegaRequestPrivate(MegaRequest::TYPE_SET_ATTR_USER, listener);
    request->setParamType(MegaApi::USER_ATTR_PWD_REMINDER);
    int numDetails = 0;
    if (lastSuccess) numDetails |= 0x01;
    if (lastSkipped) numDetails |= 0x02;
    if (mkExported) numDetails |= 0x04;
    if (dontShowAgain) numDetails |= 0x08;
    if (lastLogin) numDetails |= 0x10;
    request->setNumDetails(numDetails);

    request->performRequest = [this, request]()
    {
        return performRequest_setAttrUser(request);
    };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::changePassword(const char *oldPassword, const char *newPassword, MegaRequestListener *listener)
{
    MegaRequestPrivate *request = new MegaRequestPrivate(MegaRequest::TYPE_CHANGE_PW, listener);
    request->setPassword(oldPassword);
    request->setNewPassword(newPassword);

    request->performRequest = [this, request]()
    {
        return performRequest_changePw(request);
    };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::logout(bool keepSyncConfigsFile, MegaRequestListener *listener)
{
    cancel_epoch_bump();
    MegaRequestPrivate *request = new MegaRequestPrivate(MegaRequest::TYPE_LOGOUT, listener);
    request->setFlag(true);
    request->setTransferTag(keepSyncConfigsFile);

    request->performRequest = [this, request]()
    {
        return performRequest_logout(request);
    };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::localLogout(MegaRequestListener *listener)
{
    cancel_epoch_bump();
    MegaRequestPrivate *request = new MegaRequestPrivate(MegaRequest::TYPE_LOGOUT, listener);
    request->setFlag(false);

    request->performRequest = [this, request]()
    {
        return performRequest_logout(request);
    };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::invalidateCache()
{
    SdkMutexGuard g(sdkMutex);
    nocache = true;
}

int MegaApiImpl::getPasswordStrength(const char *password)
{
    if (!password || strlen(password) < 8)
    {
        return MegaApi::PASSWORD_STRENGTH_VERYWEAK;
    }

    double entrophy = ZxcvbnMatch(password, NULL, NULL);
    if (entrophy > 75)
    {
        return MegaApi::PASSWORD_STRENGTH_STRONG;
    }
    if (entrophy > 50)
    {
        return MegaApi::PASSWORD_STRENGTH_GOOD;
    }
    if (entrophy > 40)
    {
        return MegaApi::PASSWORD_STRENGTH_MEDIUM;
    }
    if (entrophy > 15)
    {
        return MegaApi::PASSWORD_STRENGTH_WEAK;
    }
    return MegaApi::PASSWORD_STRENGTH_VERYWEAK;
}

char* MegaApiImpl::generateRandomCharsPassword(bool uU, bool uD, bool uS, unsigned int len)
{
    const std::string pwd = MegaClient::generatePasswordChars(uU, uD, uS, len);
    return pwd.empty() ? nullptr : MegaApi::strdup(pwd.c_str());
}

void MegaApiImpl::setNodeAttribute(MegaNode *node, int type, const char *srcFilePath, MegaHandle attributehandle, MegaRequestListener *listener)
{
    MegaRequestPrivate *request = new MegaRequestPrivate(MegaRequest::TYPE_SET_ATTR_FILE, listener);
    if (srcFilePath) request->setFile(srcFilePath);
    request->setNumber(static_cast<long long>(srcFilePath ? INVALID_HANDLE : attributehandle));
    request->setParamType(type);
    request->setNodeHandle(node ? node->getHandle() : INVALID_HANDLE);
    request->setMegaBackgroundMediaUploadPtr(nullptr);

    request->performRequest = [this, request]()
    {
        return performRequest_setAttrFile(request);
    };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::putNodeAttribute(MegaBackgroundMediaUpload* bu, int type, const char *srcFilePath, MegaRequestListener *listener)
{
    MegaRequestPrivate *request = new MegaRequestPrivate(MegaRequest::TYPE_SET_ATTR_FILE, listener);
    request->setFile(srcFilePath);
    request->setParamType(type);
    request->setMegaBackgroundMediaUploadPtr(bu);
    request->setNumber(static_cast<long long>(INVALID_HANDLE));
    request->setParentHandle(INVALID_HANDLE);

    request->performRequest = [this, request]()
    {
        return performRequest_setAttrFile(request);
    };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::getUserAttr(const char *email_or_handle, int type, const char *dstFilePath, int number, MegaRequestListener *listener)
{
    MegaRequestPrivate *request = new MegaRequestPrivate(MegaRequest::TYPE_GET_ATTR_USER, listener);

    if (type == MegaApi::USER_ATTR_AVATAR && dstFilePath && *dstFilePath)
    {
        string path(dstFilePath);

        int c = path[path.size()-1];
        if((c=='/') || (c == '\\'))
        {
            path.append(email_or_handle);
            path.push_back(static_cast<char>('0' + type));
            path.append(".jpg");
        }

        request->setFile(path.c_str());
    }

    request->setParamType(type);
    request->setNumber(number);
    if(email_or_handle)
    {
        request->setEmail(email_or_handle);
    }

    request->performRequest = [this, request]()
    {
        return performRequest_getAttrUser(request);
    };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::getChatUserAttr(const char *email_or_handle, int type, const char *dstFilePath, const char *ph, int number, MegaRequestListener *listener)
{
    MegaRequestPrivate *request = new MegaRequestPrivate(MegaRequest::TYPE_GET_ATTR_USER, listener);

    if (type == MegaApi::USER_ATTR_AVATAR && dstFilePath)
    {
        string path(dstFilePath);

        int c = path[path.size()-1];
        if((c=='/') || (c == '\\'))
        {
            path.append(email_or_handle);
            path.push_back(static_cast<char>('0' + type));
            path.append(".jpg");
        }

        request->setFile(path.c_str());
    }

    request->setSessionKey(ph);
    request->setParamType(type);
    request->setNumber(number);
    if(email_or_handle)
    {
        request->setEmail(email_or_handle);
    }

    request->performRequest = [this, request]()
    {
        return performRequest_getAttrUser(request);
    };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::setUserAttr(int type, const char *value, MegaRequestListener *listener)
{
    MegaRequestPrivate *request = new MegaRequestPrivate(MegaRequest::TYPE_SET_ATTR_USER, listener);
    if(type == MegaApi::USER_ATTR_AVATAR)
    {
        request->setFile(value);
    }
    else
    {
        request->setText(value);
    }

    request->setParamType(type);

    request->performRequest = [this, request]()
    {
        return performRequest_setAttrUser(request);
    };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::getUserAttr(User* user, attr_t type, MegaRequestPrivate* request)
{
    assert(request);
    client->getua(
        user,
        type,
        -1,
        [this, request](error e)
        {
            getua_completion(e, request);
        },
        [this, request](byte* data, unsigned len, attr_t type)
        {
            getua_completion(data, len, type, request);
        },
        [this, request](unique_ptr<string_map> records, attr_t type)
        {
            getua_completion(std::move(records), type, request);
        });
}

void MegaApiImpl::getUserAttr(const string& email, attr_t type, const char* ph, MegaRequestPrivate* request)
{
    assert(request);
    client->getua(
        email.c_str(),
        type,
        ph,
        -1,
        [this, request](error e)
        {
            getua_completion(e, request);
        },
        [this, request](byte* data, unsigned len, attr_t type)
        {
            getua_completion(data, len, type, request);
        },
        [this, request](unique_ptr<string_map> records, attr_t type)
        {
            getua_completion(std::move(records), type, request);
        });
}

char *MegaApiImpl::getAvatarColor(handle userhandle)
{
    string colors[] = {
        "#55D2F0", // Blue
        "#BC2086", // Eggplant
        "#FFD200", // Gold
        "#5FDB00", // Green
        "#00BDB2", // Jade
        "#FFA700", // Orange
        "#E4269B", // Purple
        "#FF626C", // Red
        "#FF8989", // Salmon
        "#9AEAFF", // Sky
        "#00D5E2", // Teal
        "#FFEB00"  // Yellow
    };

    auto index = userhandle % (handle)(sizeof(colors)/sizeof(colors[0]));

    return MegaApi::strdup(colors[index].c_str());
}

char *MegaApiImpl::getAvatarSecondaryColor(handle userhandle)
{
    string colors[] = {
        "#2BA6DE", // Blue
        "#880E4F", // Eggplant
        "#FFA500", // Gold
        "#31B500", // Green
        "#00897B", // Jade
        "#FF6F00", // Orange
        "#C51162", // Purple
        "#FF333A", // Red
        "#FF5252", // Salmon
        "#61D2FF", // Sky
        "#00ACC1", // Teal
        "#FFD300"  // Yellow
    };

    auto index = userhandle % (handle)(sizeof(colors)/sizeof(colors[0]));

    return MegaApi::strdup(colors[index].c_str());
}

bool MegaApiImpl::isGlobalNotifiable(MegaPushNotificationSettingsPrivate* pushSettings)
{
    return !pushSettings || (!pushSettings->isGlobalDndEnabled() && isScheduleNotifiable(pushSettings));
}

bool MegaApiImpl::isScheduleNotifiable(MegaPushNotificationSettingsPrivate* pushSettings)
{
    if (!mTimezones)
    {
        LOG_warn << "Timezones are not available yet";
        return true;
    }

    if (!pushSettings || !pushSettings->isGlobalScheduleEnabled())
    {
        return true;
    }

    // find the configured timezone for notification's schedule and get the corresponding offset based on UTC
    int offsetTz = 0;
    bool tzFound = false;
    for (int i = 0; i < mTimezones->getNumTimeZones(); i++)
    {
        if (strcmp(pushSettings->getGlobalScheduleTimezone(), mTimezones->getTimeZone(i)) == 0)
        {
            offsetTz = mTimezones->getTimeOffset(i);
            tzFound = true;
            break;
        }
    }

    if (!tzFound)
    {
        LOG_err << "Timezone not found: " << pushSettings->getGlobalScheduleTimezone();
        assert(false);
        return true;    // better to generate the notification, in this case
    }

    // calculate the timestamp for time 00:00:00 of the current day in the configured timezone
    m_time_t now = m_time(NULL) + offsetTz;
    struct tm tmp;
    m_gmtime(now, &tmp);
    tmp.tm_hour = tmp.tm_min = tmp.tm_sec = 0;  // set the time to 00:00:00
    m_time_t dayStart = m_mktime_UTC(&tmp);

    // calculate the timestamps for the scheduled period
    int offsetStart = pushSettings->getGlobalScheduleStart() * 60; // convert minutes into seconds
    int offsetEnd = pushSettings->getGlobalScheduleEnd() * 60;
    m_time_t scheduleStart = dayStart + offsetStart;
    m_time_t scheduleEnd = dayStart + offsetEnd;

    if (offsetStart <= offsetEnd)
    {
        return now >= scheduleStart && now <= scheduleEnd;
    }
    else    // the scheduled period covers 2 days
    {
        assert(now >= dayStart && now <= dayStart + 24 * 60 * 60);
        return now <= scheduleEnd || now >= scheduleStart;
    }
}

// clears backups/requests/transfers notifying failure with EACCESS (and resets total up/down bytes)
void MegaApiImpl::abortPendingActions(error preverror)
{
    if (!preverror)
    {
        preverror = API_EACCESS;
    }

    // -- Backups --
    for (auto it : backupsMap)
    {
        delete it.second;
    }
    backupsMap.clear();

    // -- CS Requests in progress --
    deque<MegaRequestPrivate*> requests;
    for (auto requestPair : requestMap)
    {
        if (requestPair.second)
        {
            requests.push_back(requestPair.second);
        }
    }
    for (auto request : requests)
    {
        if (request->getType() == MegaRequest::TYPE_DELETE)
        {
            continue; // this request is deleted in MegaApiImpl dtor, its finish is the Impl destructor exiting.
        }
        fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(preverror));
    }
    requestMap.clear();

    // -- Transfers in progress --
    {
        // -- Transfers in the queue --
        // clear queued transfers, not yet started (and not added to cache)
        while (MegaTransferPrivate *transfer = transferQueue.pop())
        {
            fireOnTransferStart(transfer);
            transfer->setState(MegaTransfer::STATE_FAILED);
            fireOnTransferFinish(transfer, std::make_unique<MegaErrorPrivate>(preverror));
        }

        // clear existing transfers
        while (!transferMap.empty())
        {
            MegaTransferPrivate* transfer = transferMap.begin()->second;
            if (transfer->isRecursive() && transfer->getTotalRecursiveOperation())  // None subtransfer has been created yet (scan period)
            {
                // just remove it from the map.  When its last dependent transfer is deleted
                // then it will have its fireOnTransferFinish called also.
                transferMap.erase(transferMap.begin());
            }
            else
            {
                if (transfer->isRecursive())
                {
                    transfer->stopRecursiveOperationThread();
                }

                transfer->setState(MegaTransfer::STATE_FAILED);
                fireOnTransferFinish(transfer, std::make_unique<MegaErrorPrivate>(preverror));
            }
        }
        assert(transferMap.empty());
        transferMap.clear();
    }
}

bool MegaApiImpl::hasToForceUpload(const Node& node, const MegaTransferPrivate& transfer) const
{
    const string name = node.displayname();
    const auto lp = LocalPath::fromRelativePath(name);
    const auto hasPreview = (Node::hasfileattribute(&node.fileattrstring, GfxProc::PREVIEW) != 0);
    const auto hasThumbnail =
        (Node::hasfileattribute(&node.fileattrstring, GfxProc::THUMBNAIL) != 0);
    const auto isMedia = gfxAccess && (gfxAccess->isgfx(lp) || gfxAccess->isvideo(lp));
    const auto canForceUpload = transfer.isForceNewUpload();
    const auto isPdf = name.find(".pdf") != string::npos;

    if (canForceUpload && (isMedia || isPdf) && !(hasPreview && hasThumbnail))
    {
        LOG_debug << "[MegaApiImpl::hasToForceUpload] Force to upload a local file (Media type or "
                     "Pdf) if previous node in cloud doesn't have but thumbnail or preview..."
                  << " [handle = " << node.nodeHandle() << "]";
        return true;
    }
    if (node.hasZeroKey())
    {
        // If the node has a zerokey, we need to discard it, regardless other conditions.
        LOG_warn << "[MegaApiImpl::hasToForceUpload] Node has a zerokey, forcing a new upload..."
                 << " [handle = " << node.nodeHandle() << "]";
        client->sendevent(99486, "Node has a zerokey");
        return true;
    }
    return false;
}

void MegaApiImpl::moveTransferUp(int transferTag, MegaRequestListener *listener)
{
    MegaRequestPrivate *request = new MegaRequestPrivate(MegaRequest::TYPE_MOVE_TRANSFER, listener);
    request->setTransferTag(transferTag);
    request->setFlag(true);
    request->setNumber(MegaTransfer::MOVE_TYPE_UP);

    request->performTransferRequest = [this, request](TransferDbCommitter& committer)
    {
        return performTransferRequest_moveTransfer(request, committer);
    };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::moveTransferDown(int transferTag, MegaRequestListener *listener)
{
    MegaRequestPrivate *request = new MegaRequestPrivate(MegaRequest::TYPE_MOVE_TRANSFER, listener);
    request->setTransferTag(transferTag);
    request->setFlag(true);
    request->setNumber(MegaTransfer::MOVE_TYPE_DOWN);

    request->performTransferRequest = [this, request](TransferDbCommitter& committer)
    {
        return performTransferRequest_moveTransfer(request, committer);
    };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::moveTransferToFirst(int transferTag, MegaRequestListener *listener)
{
    MegaRequestPrivate *request = new MegaRequestPrivate(MegaRequest::TYPE_MOVE_TRANSFER, listener);
    request->setTransferTag(transferTag);
    request->setFlag(true);
    request->setNumber(MegaTransfer::MOVE_TYPE_TOP);

    request->performTransferRequest = [this, request](TransferDbCommitter& committer)
    {
        return performTransferRequest_moveTransfer(request, committer);
    };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::moveTransferToLast(int transferTag, MegaRequestListener *listener)
{
    MegaRequestPrivate *request = new MegaRequestPrivate(MegaRequest::TYPE_MOVE_TRANSFER, listener);
    request->setTransferTag(transferTag);
    request->setFlag(true);
    request->setNumber(MegaTransfer::MOVE_TYPE_BOTTOM);

    request->performTransferRequest = [this, request](TransferDbCommitter& committer)
    {
        return performTransferRequest_moveTransfer(request, committer);
    };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::moveTransferBefore(int transferTag, int prevTransferTag, MegaRequestListener *listener)
{
    MegaRequestPrivate *request = new MegaRequestPrivate(MegaRequest::TYPE_MOVE_TRANSFER, listener);
    request->setTransferTag(transferTag);
    request->setFlag(false);
    request->setNumber(prevTransferTag);

    request->performTransferRequest = [this, request](TransferDbCommitter& committer)
    {
        return performTransferRequest_moveTransfer(request, committer);
    };

    requestQueue.push(request);
    waiter->notify();
}

bool MegaApiImpl::areTransfersPaused(int direction)
{
    if(direction != MegaTransfer::TYPE_DOWNLOAD && direction != MegaTransfer::TYPE_UPLOAD)
    {
        return false;
    }

    bool result;
    SdkMutexGuard g(sdkMutex);
    if(direction == MegaTransfer::TYPE_DOWNLOAD)
    {
        result = client->xferpaused[GET];
    }
    else
    {
        result = client->xferpaused[PUT];
    }
    return result;
}

void MegaApiImpl::setDownloadMethod(int method)
{
    switch(method)
    {
        case MegaApi::TRANSFER_METHOD_NORMAL:
            client->usealtdownport = false;
            client->autodownport = false;
            break;
        case MegaApi::TRANSFER_METHOD_ALTERNATIVE_PORT:
            client->usealtdownport = true;
            client->autodownport = false;
            break;
        case MegaApi::TRANSFER_METHOD_AUTO:
            client->autodownport = true;
            break;
        case MegaApi::TRANSFER_METHOD_AUTO_NORMAL:
            client->usealtdownport = false;
            client->autodownport = true;
            break;
        case MegaApi::TRANSFER_METHOD_AUTO_ALTERNATIVE:
            client->usealtdownport = true;
            client->autodownport = true;
            break;
        default:
            break;
    }
}

void MegaApiImpl::setUploadMethod(int method)
{
    switch(method)
    {
        case MegaApi::TRANSFER_METHOD_NORMAL:
            client->usealtupport = false;
            client->autoupport = false;
            break;
        case MegaApi::TRANSFER_METHOD_ALTERNATIVE_PORT:
            client->usealtupport = true;
            client->autoupport = false;
            break;
        case MegaApi::TRANSFER_METHOD_AUTO:
            client->autoupport = true;
            break;
        case MegaApi::TRANSFER_METHOD_AUTO_NORMAL:
            client->usealtupport = false;
            client->autoupport = true;
            break;
        case MegaApi::TRANSFER_METHOD_AUTO_ALTERNATIVE:
            client->usealtupport = true;
            client->autoupport = true;
            break;
        default:
            break;
    }
}

bool MegaApiImpl::setMaxDownloadSpeed(m_off_t bpslimit)
{
    SdkMutexGuard g(sdkMutex);
    return client->setmaxdownloadspeed(bpslimit);
}

bool MegaApiImpl::setMaxUploadSpeed(m_off_t bpslimit)
{
    SdkMutexGuard g(sdkMutex);
    return client->setmaxuploadspeed(bpslimit);
}

int MegaApiImpl::getMaxDownloadSpeed()
{
    return int(client->getmaxdownloadspeed());
}

int MegaApiImpl::getMaxUploadSpeed()
{
    return int(client->getmaxuploadspeed());
}

int MegaApiImpl::getCurrentDownloadSpeed()
{
    return int(httpio->downloadSpeed);
}

int MegaApiImpl::getCurrentUploadSpeed()
{
    return int(httpio->uploadSpeed);
}

int MegaApiImpl::getCurrentSpeed(int type)
{
    switch (type)
    {
    case MegaTransfer::TYPE_DOWNLOAD:
        return int(httpio->downloadSpeed);
    case MegaTransfer::TYPE_UPLOAD:
        return int(httpio->uploadSpeed);
    default:
        return 0;
    }
}

int MegaApiImpl::getDownloadMethod()
{
    if (client->autodownport)
    {
        if(client->usealtdownport)
        {
            return MegaApi::TRANSFER_METHOD_AUTO_ALTERNATIVE;
        }
        else
        {
            return MegaApi::TRANSFER_METHOD_AUTO_NORMAL;
        }
    }

    if (client->usealtdownport)
    {
        return MegaApi::TRANSFER_METHOD_ALTERNATIVE_PORT;
    }

    return MegaApi::TRANSFER_METHOD_NORMAL;
}

int MegaApiImpl::getUploadMethod()
{
    if (client->autoupport)
    {
        if(client->usealtupport)
        {
            return MegaApi::TRANSFER_METHOD_AUTO_ALTERNATIVE;
        }
        else
        {
            return MegaApi::TRANSFER_METHOD_AUTO_NORMAL;
        }
    }

    if (client->usealtupport)
    {
        return MegaApi::TRANSFER_METHOD_ALTERNATIVE_PORT;
    }

    return MegaApi::TRANSFER_METHOD_NORMAL;
}

MegaTransferData *MegaApiImpl::getTransferData(MegaTransferListener *listener)
{
    SdkMutexGuard g(sdkMutex);
    MegaTransferData *data = new MegaTransferDataPrivate(&client->transferlist, notificationNumber);
    if (listener)
    {
        transferListeners.insert(listener);
    }
    return data;
}

MegaTransfer *MegaApiImpl::getFirstTransfer(int type)
{
    if (type != MegaTransfer::TYPE_DOWNLOAD && type != MegaTransfer::TYPE_UPLOAD)
    {
        return NULL;
    }

    MegaTransfer* transfer = NULL;
    SdkMutexGuard g(sdkMutex);
    auto it = client->transferlist.begin((direction_t)type);
    if (it != client->transferlist.end((direction_t)type))
    {
         Transfer *t = (*it);
         if (t->files.size())
         {
             MegaTransferPrivate *megaTransfer = getMegaTransferPrivate(t->files.front()->tag);
             if (megaTransfer)
             {
                 transfer = megaTransfer->copy();
             }
         }
    }
    return transfer;
}

void MegaApiImpl::notifyTransfer(int transferTag, MegaTransferListener *listener)
{
    SdkMutexGuard g(sdkMutex);
    MegaTransferPrivate *t = getMegaTransferPrivate(transferTag);
    if (!t)
    {
        return;
    }

    fireOnTransferUpdate(t);
    if (listener)
    {
        listener->onTransferUpdate(api, t);
    }
}

MegaTransferList *MegaApiImpl::getTransfers()
{
    SdkMutexGuard g(sdkMutex);
    vector<MegaTransfer *> transfers;
    for (int d = GET; d == GET || d == PUT; d += PUT - GET)
    {
        auto end = client->transferlist.end((direction_t)d);
        for (auto it = client->transferlist.begin((direction_t)d); it != end; it++)
        {
            Transfer *t = (*it);
            for (file_list::iterator it2 = t->files.begin(); it2 != t->files.end(); it2++)
            {
                MegaTransferPrivate* transfer = getMegaTransferPrivate((*it2)->tag);
                if (transfer)
                {
                    transfers.push_back(transfer);
                }
            }
        }
    }
    return new MegaTransferListPrivate(transfers.data(), int(transfers.size()));
}

MegaTransferList *MegaApiImpl::getStreamingTransfers()
{
    SdkMutexGuard g(sdkMutex);

    vector<MegaTransfer *> transfers;
    for (std::map<int, MegaTransferPrivate *>::iterator it = transferMap.begin(); it != transferMap.end(); it++)
    {
        MegaTransferPrivate *transfer = it->second;
        if (transfer->isStreamingTransfer())
        {
            transfers.push_back(transfer);
        }
    }
    return new MegaTransferListPrivate(transfers.data(), int(transfers.size()));
}

MegaTransfer* MegaApiImpl::getTransferByUniqueId(uint32_t transferUniqueId) const
{
    SdkMutexGuard g(sdkMutex);
    const auto it = std::find_if(begin(transferMap),
                                 end(transferMap),
                                 [&id = transferUniqueId](const auto& p)
                                 {
                                     return p.second->getUniqueId() == id;
                                 });

    return it != end(transferMap) ? it->second->copy() : nullptr;
}

MegaTransfer *MegaApiImpl::getTransferByTag(int transferTag)
{
    SdkMutexGuard g(sdkMutex);
    MegaTransfer* value = getMegaTransferPrivate(transferTag);
    if (value)
    {
        value = value->copy();
    }
    return value;
}

MegaTransferList *MegaApiImpl::getTransfers(int type)
{
    if(type != MegaTransfer::TYPE_DOWNLOAD && type != MegaTransfer::TYPE_UPLOAD)
    {
        return new MegaTransferListPrivate();
    }

    SdkMutexGuard g(sdkMutex);
    vector<MegaTransfer *> transfers;
    auto end = client->transferlist.end((direction_t)type);
    for (auto it = client->transferlist.begin((direction_t)type); it != end; it++)
    {
        Transfer *t = (*it);
        for (file_list::iterator it2 = t->files.begin(); it2 != t->files.end(); it2++)
        {
            MegaTransferPrivate* transfer = getMegaTransferPrivate((*it2)->tag);
            if (transfer)
            {
                transfers.push_back(transfer);
            }
        }
    }
    return new MegaTransferListPrivate(transfers.data(), int(transfers.size()));
}

MegaTransferList *MegaApiImpl::getChildTransfers(int transferTag)
{
    SdkMutexGuard g(sdkMutex);

    MegaTransfer *transfer = getMegaTransferPrivate(transferTag);
    if (!transfer)
    {
        return new MegaTransferListPrivate();
    }

    if (!transfer->isFolderTransfer())
    {
        return new MegaTransferListPrivate();
    }

    vector<MegaTransfer *> transfers;
    for (std::map<int, MegaTransferPrivate *>::iterator it = transferMap.begin(); it != transferMap.end(); it++)
    {
        MegaTransferPrivate *t = it->second;
        if (t->getFolderTransferTag() == transferTag)
        {
            transfers.push_back(transfer);
        }
    }

    return new MegaTransferListPrivate(transfers.data(), int(transfers.size()));
}

MegaTransferList *MegaApiImpl::getTansfersByFolderTag(int folderTransferTag)
{
    SdkMutexGuard g(sdkMutex);
    vector<MegaTransfer *> transfers;
    for (std::map<int, MegaTransferPrivate *>::iterator it = transferMap.begin(); it != transferMap.end(); it++)
    {
        MegaTransferPrivate *t = it->second;
        if (t->getFolderTransferTag() == folderTransferTag)
        {
            transfers.push_back(t);
        }
    }

    return new MegaTransferListPrivate(transfers.data(), int(transfers.size()));
}

MegaStringList *MegaApiImpl::getBackupFolders(int backuptag)
{
    map<int64_t, string> backupTimesPaths;
    {
        SdkMutexGuard g(sdkMutex);

        map<int, MegaScheduledCopyController *>::iterator itr = backupsMap.find(backuptag) ;
        if (itr == backupsMap.end())
        {
            LOG_err << "Failed to find backup with tag " << backuptag;
            return NULL;
        }

        MegaScheduledCopyController *mbc = itr->second;

        MegaNode * parentNode = getNodeByHandle(mbc->getMegaHandle());
        if (parentNode)
        {
            MegaNodeList* children = getChildren(parentNode, MegaApi::ORDER_NONE);
            if (children)
            {
                for (int i = 0; i < children->size(); i++)
                {
                    MegaNode *childNode = children->get(i);
                    string childname = childNode->getName();
                    if (mbc->isBackup(childname, mbc->getBackupName()) )
                    {
                        int64_t timeofbackup = mbc->getTimeOfBackup(childname);
                        if (timeofbackup)
                        {
                            backupTimesPaths[timeofbackup]=getNodePath(childNode);
                        }
                        else
                        {
                            LOG_err << "Failed to get backup time for folder: " << childname << ". Discarded.";
                        }
                    }
                }

                delete children;
            }
            delete parentNode;
        }
    }

    string_vector listofpaths;

    for(map<int64_t, string>::iterator itr = backupTimesPaths.begin(); itr != backupTimesPaths.end(); itr++)
    {
        listofpaths.push_back(itr->second);
    }
    return new MegaStringListPrivate(std::move(listofpaths));
}

void MegaApiImpl::abortCurrentScheduledCopy(int tag, MegaRequestListener *listener)
{
    MegaRequestPrivate *request = new MegaRequestPrivate(MegaRequest::TYPE_ABORT_CURRENT_SCHEDULED_COPY, listener);
    request->setNumber(tag);

    request->performRequest = [this, request]()
    {
        return processAbortBackupRequest(request);
    };

    requestQueue.push(request);
    waiter->notify();
}

MegaTransferPrivate* MegaApiImpl::createUploadTransfer(const LocalPath& localPath,
                                                       MegaHandle parentHandle,
                                                       const MegaUploadOptionsPrivate& options,
                                                       CancelToken cancelToken,
                                                       MegaTransferListener* listener,
                                                       const FileFingerprint* preFingerprintedFile)
{
    FileSystemType fsType = options.mFsType;
    if (fsType == FS_UNKNOWN)
    {
        fsType = fsAccess->getlocalfstype(localPath);
    }
    const char* fileName =
        options.mPublicOptions.fileName.empty() ? nullptr : options.mPublicOptions.fileName.c_str();
    const char* targetUser = options.mTargetUser.empty() ? nullptr : options.mTargetUser.c_str();
    const int64_t mtime = options.mPublicOptions.mtime;
    const int folderTransferTag = options.mFolderTransferTag;

    MegaTransferPrivate* transfer = new MegaTransferPrivate(MegaTransfer::TYPE_UPLOAD, listener);
    if (!localPath.empty())
    {
        transfer->setLocalPath(localPath);
    }

    if (parentHandle != INVALID_HANDLE)
    {
        transfer->setParentHandle(parentHandle);
    }

    if (targetUser)
    {
        transfer->setParentPath(targetUser);
    }

    transfer->setMaxRetries(maxRetries);
    transfer->setAppData(options.mPublicOptions.appData);
    transfer->setSourceFileTemporary(options.mPublicOptions.isSourceTemporary);
    transfer->setStartFirst(options.mPublicOptions.startFirst);
    transfer->setCancelToken(cancelToken);
    transfer->setBackupTransfer(options.mIsBackup);

    if (fileName || transfer->getFileName())
    {
        string auxName = fileName ? fileName : transfer->getFileName();

        client->fsaccess->unescapefsincompatible(&auxName);
        transfer->setFileName(auxName.c_str());
    }

    transfer->setTime(mtime);

    if (preFingerprintedFile)
    {
        transfer->fingerprint_error = preFingerprintedFile->isvalid ? API_OK : API_EREAD;
        transfer->fingerprint_filetype = FILENODE;
        transfer->fingerprint_onDisk = *preFingerprintedFile;
    }
    else // if no fingerprint provided, calculate it, to avoid extra workload (and reduce mutex locking time) to SDK thread at sendPendingTransfers
    {
        lock_guard<mutex> g(fingerprintingFsAccessMutex);
        assert(fingerprintingFsAccess); // somehow ::init() wasn't evaluated
        auto fa = fingerprintingFsAccess->newfileaccess();

        if (localPath.empty() || !fa->fopen(localPath, OPEN_RDONLY, FSLogging::logOnError))
        {
            transfer->fingerprint_error = API_EREAD;
        }
        else
        {
            transfer->fingerprint_filetype = fa->type; // => [FILENODE | FOLDERNODE]

            auto uploadToInbox = transfer->getInboxTarget();
            if (fa->type == FOLDERNODE && uploadToInbox.has_value())
            {
                //Folder upload is not possible when sending to Inbox:
                //API won't return handle for folder creation, and even if that was the case
                //doing a put nodes with t = userhandle & the corresponding handle as parent p, API returns EACCESS
                transfer->fingerprint_error = API_EREAD;
            }
            else
            {
                transfer->fingerprint_error = API_OK;
            }

            if (fa->type == FILENODE) // just file nodes have a valid fingerprint
            {
                transfer->fingerprint_onDisk.genfingerprint(fa.get());
            }
        }
    }

    if(folderTransferTag)
    {
        transfer->setFolderTransferTag(folderTransferTag);
    }

    transfer->setForceNewUpload(options.mForceNewUpload);
    return transfer;
}

void MegaApiImpl::startUpload(const std::string localPath,
                              MegaHandle parentHandle,
                              CancelToken cancelToken,
                              const MegaUploadOptionsPrivate& options,
                              MegaTransferListener* listener)
{
    LocalPath path;
    if (!localPath.empty())
    {
        path = LocalPath::fromAbsolutePath(localPath.c_str());
    }

    const PitagTrigger pitagTrigger = pitagTriggerFromChar(options.mPublicOptions.pitagTrigger);
    const PitagTarget pitagTarget = pitagTargetFromChar(options.mPublicOptions.pitagTarget);
    MegaTransferPrivate* transfer =
        createUploadTransfer(path, parentHandle, options, cancelToken, listener);

    const auto nodeType =
        (transfer->fingerprint_filetype == FILENODE) ? PitagNodeType::File : PitagNodeType::Folder;
    const Pitag pitag{PitagPurpose::Upload,
                      pitagTrigger,
                      nodeType,
                      pitagTarget,
                      PitagImportSource::NotApplicable};
    transfer->setPitag(pitag);
    transferQueue.push(transfer);
    waiter->notify();
}

void MegaApiImpl::startUploadForSupport(const char* localPath, bool isSourceFileTemporary, FileSystemType fsType, MegaTransferListener* listener)
{
    LocalPath path;
    if (localPath)
    {
        path = LocalPath::fromAbsolutePath(localPath);
    }

    MegaUploadOptionsPrivate options;
    options.mPublicOptions.startFirst = true;
    options.mPublicOptions.mtime = MegaApi::INVALID_CUSTOM_MOD_TIME;
    options.mPublicOptions.isSourceTemporary = isSourceFileTemporary;
    options.mFsType = fsType;
    options.mTargetUser = MegaClient::SUPPORT_USER_HANDLE;

    MegaTransferPrivate* transfer =
        createUploadTransfer(path, INVALID_HANDLE, options, CancelToken(), listener);

    transferQueue.push(transfer);
    waiter->notify();
}

void MegaApiImpl::startDownload (bool startFirst, MegaNode *node, const char* localPath, const char *customName, int folderTransferTag, const char *appData, CancelToken cancelToken, int collisionCheck, int collisionResolution, bool undelete, MegaTransferListener *listener)
{
    LocalPath path;
    if (localPath)
    {
        path = LocalPath::fromAbsolutePath(localPath);
    }

    FileSystemType fsType = fsAccess->getlocalfstype(path);
    MegaTransferPrivate* transfer = createDownloadTransfer(startFirst,
                                                           node,
                                                           path,
                                                           customName,
                                                           folderTransferTag,
                                                           appData,
                                                           cancelToken,
                                                           collisionCheck,
                                                           collisionResolution,
                                                           undelete,
                                                           listener,
                                                           fsType);
    transferQueue.push(transfer);
    waiter->notify();
}

MegaTransferPrivate* MegaApiImpl::createDownloadTransfer(bool startFirst,
                                                         MegaNode* node,
                                                         const LocalPath& localPath,
                                                         const char* customName,
                                                         int folderTransferTag,
                                                         const char* appData,
                                                         CancelToken cancelToken,
                                                         int collisionCheck,
                                                         int collisionResolution,
                                                         bool undelete,
                                                         MegaTransferListener* listener,
                                                         FileSystemType fsType)
{
    assert(!undelete || node);

    MegaTransferPrivate* transfer = new MegaTransferPrivate(MegaTransfer::TYPE_DOWNLOAD, listener);

    if (!localPath.empty())
    {
        if (localPath.endsInSeparator())
        {
            transfer->setParentPath(localPath.toPath(false).c_str());
        }
        else
        {
            transfer->setLocalPath(localPath);
        }
    }

    if (node)
    {
        transfer->setNodeHandle(node->getHandle());
        if (undelete)
        {
            transfer->setNodeToUndelete(node);
        }
        else if (node->isPublic() || node->isForeign())
        {
            transfer->setPublicNode(node, true);
        }
    }

    transfer->setMaxRetries(maxRetries);
    transfer->setAppData(appData);
    transfer->setStartFirst(startFirst);
    transfer->setCancelToken(cancelToken);
    transfer->setCollisionCheck(collisionCheck);
    transfer->setCollisionResolution(collisionResolution);

    // cache fsType to transfer as get fsType on a network driver could be expensive
    transfer->setFileSystemType(fsType);

    if (customName)
    {
       // set custom file/folder name if exists and escape incompatible characters for destination FS
       std::string auxName = customName;
       client->fsaccess->escapefsincompatible(&auxName, fsType);
       transfer->setFileName(auxName.c_str());
    }

    if (folderTransferTag)
    {
        transfer->setFolderTransferTag(folderTransferTag);
    }

    return transfer;
}

void MegaApiImpl::cancelTransfer(MegaTransfer *t, MegaRequestListener *listener)
{
    MegaRequestPrivate *request = new MegaRequestPrivate(MegaRequest::TYPE_CANCEL_TRANSFER, listener);
    if(t)
    {
        request->setTransferTag(t->getTag());
    }

    request->performTransferRequest = [this, request](TransferDbCommitter& committer)
    {
        return performTransferRequest_cancelTransfer(request, committer);
    };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::cancelTransferByTag(int transferTag, MegaRequestListener *listener)
{
    MegaRequestPrivate *request = new MegaRequestPrivate(MegaRequest::TYPE_CANCEL_TRANSFER, listener);
    request->setTransferTag(transferTag);

    request->performTransferRequest = [this, request](TransferDbCommitter& committer)
    {
        return performTransferRequest_cancelTransfer(request, committer);
    };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::startStreaming(MegaNode* node, m_off_t startPos, m_off_t size, MegaTransferListener *listener)
{
    MegaTransferPrivate* transfer = new MegaTransferPrivate(MegaTransfer::TYPE_DOWNLOAD, listener);

    if (node)
    {
        transfer->setNodeHandle(node->getHandle());
        if (node->isPublic() || node->isForeign())
        {
            transfer->setPublicNode(node, true);
        }
    }

    transfer->setStreamingTransfer(true);
    transfer->setStartPos(startPos);
    transfer->setEndPos(startPos + size - 1);
    transfer->setMaxRetries(maxRetries);
    transferQueue.push(transfer);
    waiter->notify();
}

void MegaApiImpl::setStreamingMinimumRate(int bytesPerSecond)
{
    SdkMutexGuard g(sdkMutex);
    LOG_debug << "Setting minimum acceptable speed for streaming: " << bytesPerSecond << "B/s";
    client->minstreamingrate = bytesPerSecond;
}

void MegaApiImpl::retryTransfer(MegaTransfer *transfer, MegaTransferListener *listener)
{
    MegaTransferPrivate *t = dynamic_cast<MegaTransferPrivate*>(transfer);
    if (!t || (t->getType() != MegaTransfer::TYPE_DOWNLOAD && t->getType() != MegaTransfer::TYPE_UPLOAD))
    {
        return;
    }

    int type = t->getType();
    if (type == MegaTransfer::TYPE_DOWNLOAD)
    {
        MegaNode *node = t->getNodeToUndelete() ? t->getNodeToUndelete() : t->getPublicMegaNode();
        if (!node)
        {
            node = getNodeByHandle(t->getNodeHandle());
        }
        this->startDownload(true,
                            node,
                            t->getPath(),
                            NULL,
                            0,
                            t->getAppData(),
                            CancelToken(),
                            static_cast<int>(t->getCollisionCheck()),
                            static_cast<int>(t->getCollisionResolution()),
                            t->getNodeToUndelete() != nullptr,
                            listener);

        delete node;
    }
    else
    {
        MegaNode *parent = getNodeByHandle(t->getParentHandle());
        Pitag pitag{t->getPitag()};
        MegaUploadOptionsPrivate options;
        if (const char* fileName = t->getFileName())
        {
            options.mPublicOptions.fileName = fileName;
        }
        options.mPublicOptions.mtime = t->getTime();
        options.mPublicOptions.appData = t->getAppData();
        options.mPublicOptions.isSourceTemporary = t->isSourceFileTemporary();
        options.mPublicOptions.startFirst = true;
        options.mPublicOptions.pitagTrigger = static_cast<char>(pitag.trigger);
        options.mIsBackup = t->isBackupTransfer();
        options.mForceNewUpload = t->isForceNewUpload();
        options.mFsType = client->fsaccess->getlocalfstype(t->getLocalPath());
        options.mPitagTarget = pitag.target;

        const char* transferPath = t->getPath();
        const std::string normalizedPath = transferPath ? transferPath : "";
        this->startUpload(normalizedPath,
                          parent ? parent->getHandle() : INVALID_HANDLE,
                          t->accessCancelToken(),
                          options,
                          listener);

        delete parent;
    }
}

#ifdef ENABLE_SYNC

int MegaApiImpl::syncPathState(string* platformEncoded)
{
    LocalPath localpath = LocalPath::fromPlatformEncodedAbsolute(*platformEncoded);

    int cached_ts;
    if (mRecentlyRequestedOverlayIconPaths.lookup(localpath, cached_ts) ||
        mRecentlyNotifiedOverlayIconPaths.lookup(localpath, cached_ts))
    {
        return cached_ts;
    }

    handle containingSyncId = client->syncs.getSyncIdContainingActivePath(localpath);

    if (containingSyncId == UNDEF) return MegaApi::STATE_IGNORED;

    // Avoid blocking on the mutex for a long time, as we may be blocking windows explorer (or another platform's equivalent) from opening or displaying a window, unrelated to sync folders
    // We try to lock the SDK mutex.  If we can't get it in 10ms then we return a simple default, and subsequent requests try to lock the mutex but don't wait.
    std::unique_lock<std::timed_mutex> g(client->syncs.mLocalNodeChangeMutex, std::defer_lock);
    if ((!syncPathStateLockTimeout && !g.try_lock_for(std::chrono::milliseconds(10))) ||
        (syncPathStateLockTimeout && !g.try_lock()))
    {
        // mLocalNodeChangeMutex is not locked

        if (!syncPathStateLockTimeout)
        {
            LOG_verbose << "Cannot get lock to report treestate for path " << localpath;
        }

        syncPathStateLockTimeout = true;

        // store up to 1000 paths for lookup later when we can lock mLocalNodeChangeMutex
        lock_guard<mutex> syncPathStateLock(syncPathStateDeferredSetMutex);
        if (syncPathStateDeferredSet.size() < 1024)
        {
            syncPathStateDeferredSet.insert(localpath);
        }

        return MegaApi::STATE_IGNORED;
    }

    // mLocalNodeChangeMutex is locked (and will be unlocked by unique_lock destructor)

    treestate_t ts = client->syncs.getSyncStateForLocalPath(containingSyncId, localpath);

    if (syncPathStateLockTimeout)
    {
        LOG_verbose << "Resuming reporting treestate. " << ts << " for path " << localpath;

        // once we do manage to lock, return to normal operation.
        syncPathStateLockTimeout = false;

        // issue notifications for the OS to ask about the paths we skipped while we couldn't lock the mutex
        auto tmp = std::make_shared<set<LocalPath>>();
        {
            lock_guard<mutex> syncPathStateLock(syncPathStateDeferredSetMutex);
            tmp->swap(syncPathStateDeferredSet);
        }
        LOG_verbose << "Issuing updates for OS path icon ovelays for . " << tmp->size() << " paths";
        auto mc = client;
        client->syncs.queueSync([tmp, mc](){
            LOG_verbose << "Processing updates for OS path icon ovelays for . " << tmp->size() << " paths";
            for (auto& p : *tmp)
            {
                treestate_t ts;
                nodetype_t nt;
                SyncConfig sc;
                if (mc->syncs.getSyncStateForLocalPath(p, ts, nt, sc))
                {
                    mc->syncs.queueClient(
                        [sc = std::move(sc), p, ts, nt](MegaClient& mc, TransferDbCommitter&)
                        {
                            mc.app->syncupdate_treestate(sc, p, ts, nt);
                        });
                }
            }
            LOG_verbose << "Completed updates for OS path icon ovelays for . " << tmp->size() << " paths";
        }, "syncPathState catchup after glitches");
    }

    mRecentlyRequestedOverlayIconPaths.addOrUpdate(localpath, ts);

    return ts;
}


MegaNode *MegaApiImpl::getSyncedNode(const LocalPath& path)
{
    // syncs has its own thread safety
    NodeHandle h = client->syncs.getSyncedNodeForLocalPath(path);

    SdkMutexGuard g(sdkMutex);
    return MegaNodePrivate::fromNode(client->nodeByHandle(h).get());
}

const char* MegaApiImpl::exportSyncConfigs()
{
    string configs;

    {
        SdkMutexGuard guard(sdkMutex);
        configs = client->syncs.exportSyncConfigs();
    }

    return MegaApi::strdup(configs.c_str());
}

void MegaApiImpl::setSyncRunState(MegaHandle backupId, MegaSync::SyncRunningState targetState, MegaRequestListener *listener)
{
    MegaRequestPrivate *request = new MegaRequestPrivate(MegaRequest::TYPE_SET_SYNC_RUNSTATE, listener);
    request->setParentHandle(backupId);

    request->performRequest = [this, request, targetState](){

        auto backupId = request->getParentHandle();

        switch (targetState)
        {
            case MegaSync::RUNSTATE_RUNNING:
            {
                client->syncs.enableSyncByBackupId(backupId, true, [this, request](error err, SyncError serr, handle)
                    {
                        request->setNumDetails(serr);
                        fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(err, serr), true);
                    }, false, "");

                return API_OK;
            }
            case MegaSync::RUNSTATE_SUSPENDED:
            case MegaSync::RUNSTATE_DISABLED:
            {
                if (targetState == MegaSync::SyncRunningState(SyncRunState::Pause))
                {
                    LOG_warn << "[MegaApiImpl::setSyncRunState] Target state: SyncRunState::Pause. Sync will be suspended";
                }
                bool keepSyncDb = targetState == MegaSync::SyncRunningState(SyncRunState::Pause) || targetState == MegaSync::SyncRunningState(SyncRunState::Suspend);

                client->syncs.disableSyncByBackupId(
                    backupId,
                    NO_SYNC_ERROR,
                    false,
                    keepSyncDb,
                    [this, request](){
                        fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(API_OK), true);
                    });

                return API_OK;
            }

            case MegaSync::RUNSTATE_PENDING:
            case MegaSync::RUNSTATE_LOADING:
            default:
                assert(false);
                return API_EARGS;
        }
    };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::rescanSync(MegaHandle backupId, bool reFingerprint)
{
    // no need to go via the Request queue
    // (since syncs are threaded independently, no need to lock sdkMutex)
    client->syncs.setSyncsNeedFullSync(true, reFingerprint, backupId);
}

MegaSyncList *MegaApiImpl::getSyncs()
{
    vector<MegaSyncPrivate*> vMegaSyncs;

    // syncs has its own thread safety
    for (auto& config : client->syncs.getConfigs(false))
    {
        vMegaSyncs.push_back(new MegaSyncPrivate(config, client));
    }

    MegaSyncList *syncList = new MegaSyncListPrivate(vMegaSyncs.data(), int(vMegaSyncs.size()));

    for (auto p : vMegaSyncs) delete p;

    return syncList;
}

void MegaApiImpl::setLegacyExcludedNames(vector<string>*)
{
    return;
}

void MegaApiImpl::setLegacyExcludedPaths(vector<string>*)
{
    return;
}

void MegaApiImpl::setLegacyExclusionLowerSizeLimit(unsigned long long)
{
    return;
}

void MegaApiImpl::setLegacyExclusionUpperSizeLimit(unsigned long long)
{
    return;
}

MegaError* MegaApiImpl::exportLegacyExclusionRules(const char*)
{
    return new MegaErrorPrivate(API_EINTERNAL);
}

long long MegaApiImpl::getNumLocalNodes()
{
    return client->syncs.totalLocalNodes;
}

#endif

void MegaApiImpl::moveOrRemoveDeconfiguredBackupNodes(MegaHandle deconfiguredBackupRoot, MegaHandle backupDestination, MegaRequestListener* listener)
{
    MegaRequestPrivate* request = new MegaRequestPrivate(MegaRequest::TYPE_REMOVE_OLD_BACKUP_NODES, listener);
    request->setNodeHandle(backupDestination);

    request->performRequest = [deconfiguredBackupRoot, backupDestination, this, request]()
    {
        std::shared_ptr<Node> deconfiguredBackupRootNode =
            client->nodebyhandle(deconfiguredBackupRoot);
        std::shared_ptr<Node> backupDestinationNode = client->nodebyhandle(backupDestination);

        if (!deconfiguredBackupRootNode)
        {
            LOG_debug << "Backup root node not found";
            return API_ENOENT;
        }
        LOG_debug << "About to move/remove backup nodes from "
                  << deconfiguredBackupRootNode->displaypath();

        if (!deconfiguredBackupRootNode->parent || // device
            !deconfiguredBackupRootNode->parent->parent || // my backups node
            !deconfiguredBackupRootNode->parent->parent->parent || // Vault root
            deconfiguredBackupRootNode->parent->parent->parent->nodehandle !=
                client->mNodeManager.getRootNodeVault().as8byte())
        {
            LOG_debug << "Node not in the right place to be a backup root";
            return API_EARGS;
        }

        if (backupDestinationNode &&
            backupDestinationNode->firstancestor()->nodeHandle() !=
                client->mNodeManager.getRootNodeFiles().as8byte() &&
            backupDestinationNode->firstancestor()->nodeHandle() !=
                client->mNodeManager.getRootNodeRubbish().as8byte())
        {
            LOG_debug << "Destination node not in the main files root, or in rubbish: "
                      << backupDestinationNode->displaypath();
            return API_EARGS;
        }

        NodeHandle root = NodeHandle().set6byte(deconfiguredBackupRoot);
        NodeHandle destination = NodeHandle().set6byte(backupDestination);

        if (backupDestinationNode &&
            backupDestinationNode->hasChildWithName(deconfiguredBackupRootNode->displayname()))
        {
            LOG_err << "A node with the same name already exists in the destination. Can't move "
                       "the backup node "
                    << toNodeHandle(root) << " into " << toNodeHandle(destination);
            return API_EEXIST;
        }

        client->unlinkOrMoveBackupNodes(root, destination, [request, this](Error e) {
            fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e));
            });
        return API_OK;
    };

    requestQueue.push(request);
    waiter->notify();
}

MegaNode *MegaApiImpl::getRootNode()
{
    // return without locking the main mutex if possible.
    // (always lock for folder links, since node attributes can change)
    // Only compare fixed-location 8-byte values
    std::unique_lock<mutex> g(mLastRecievedLoggedMeMutex);
    if (client->mNodeManager.getRootNodeFiles().isUndef()) return nullptr;
    if (!mLastKnownRootNode ||
            client->loggedIntoFolder() ||
            mLastKnownRootNode->getHandle() != client->mNodeManager.getRootNodeFiles().as8byte())
    {
        // ok now lock main mutex, but not while mLastRecievedLoggedMeMutex is locked or we might get deadlocks as another thread updates mLastKnownRootNode with the mutexes locking in the other order
        g.unlock();
        MegaNode* newPtr = nullptr;
        {
            SdkMutexGuard lock(sdkMutex);
            newPtr = MegaNodePrivate::fromNode(client->nodeByHandle(client->mNodeManager.getRootNodeFiles()).get());
        }
        g.lock();
        mLastKnownRootNode.reset(newPtr);
    }

    return mLastKnownRootNode ? mLastKnownRootNode->copy() : nullptr;
}

MegaNode* MegaApiImpl::getVaultNode()
{
    // return without locking the main mutex if possible.
    // Only compare fixed-location 8-byte values
    std::unique_lock<mutex> g(mLastRecievedLoggedMeMutex);
    if (client->mNodeManager.getRootNodeVault().isUndef()) return nullptr;
    if (!mLastKnownVaultNode ||
        mLastKnownVaultNode->getHandle() != client->mNodeManager.getRootNodeVault().as8byte())
    {
        // ok now lock main mutex, but not while mLastRecievedLoggedMeMutex is locked or we might get deadlocks as another thread updates mLastKnownVaultNode with the mutexes locking in the other order
        g.unlock();
        MegaNode* newPtr = nullptr;
        {
            SdkMutexGuard lock(sdkMutex);
            newPtr = MegaNodePrivate::fromNode(client->nodeByHandle(client->mNodeManager.getRootNodeVault()).get());
        }
        g.lock();
        mLastKnownVaultNode.reset(newPtr);
    }

    return mLastKnownVaultNode ? mLastKnownVaultNode->copy() : nullptr;
}

MegaNode* MegaApiImpl::getRubbishNode()
{
    // return without locking the main mutex if possible.
    // Only compare fixed-location 8-byte values
    std::unique_lock<mutex> g(mLastRecievedLoggedMeMutex);
    if (client->mNodeManager.getRootNodeRubbish().isUndef()) return nullptr;
    if (!mLastKnownRubbishNode ||
        mLastKnownRubbishNode->getHandle() != client->mNodeManager.getRootNodeRubbish().as8byte())
    {
        // ok now lock main mutex, but not while mLastRecievedLoggedMeMutex is locked or we might get deadlocks as another thread updates mLastKnownRubbishNode with the mutexes locking in the other order
        g.unlock();
        MegaNode* newPtr = nullptr;
        {
            SdkMutexGuard lock(sdkMutex);
            newPtr = MegaNodePrivate::fromNode(client->nodeByHandle(client->mNodeManager.getRootNodeRubbish()).get());
        }
        g.lock();
        mLastKnownRubbishNode.reset(newPtr);
    }

    return mLastKnownRubbishNode ? mLastKnownRubbishNode->copy() : nullptr;
}

MegaNode *MegaApiImpl::getRootNode(MegaNode *node)
{
    MegaNode *rootnode = NULL;

    SdkMutexGuard lock(sdkMutex);

    std::shared_ptr<Node> n;
    if (node && (n = client->nodebyhandle(node->getHandle())))
    {
        while (n->parent)
        {
            n = n->parent;
        }

        rootnode = MegaNodePrivate::fromNode(n.get());
    }
    return rootnode;
}

bool MegaApiImpl::isInRootnode(MegaNode *node, int index)
{
    bool ret = false;

    SdkMutexGuard lock(sdkMutex);

    if (MegaNode *rootnode = getRootNode(node))
    {
        ret = (index == 0 && rootnode->getHandle() == client->mNodeManager.getRootNodeFiles().as8byte()) ||
              (index == 1 && rootnode->getHandle() == client->mNodeManager.getRootNodeVault().as8byte()) ||
              (index == 2 && rootnode->getHandle() == client->mNodeManager.getRootNodeRubbish().as8byte());

        delete rootnode;
    }

    return ret;
}

void MegaApiImpl::setDefaultFilePermissions(int permissions)
{
    SdkMutexGuard lock(sdkMutex);
    fsAccess->setdefaultfilepermissions(permissions);
    client->fsaccess->setdefaultfilepermissions(permissions);
#ifdef ENABLE_SYNC
    client->syncs.setdefaultfilepermissions(permissions);
#endif
}

int MegaApiImpl::getDefaultFilePermissions()
{
    return fsAccess->getdefaultfilepermissions();
}

void MegaApiImpl::setDefaultFolderPermissions(int permissions)
{
    SdkMutexGuard lock(sdkMutex);
    fsAccess->setdefaultfolderpermissions(permissions);
    client->fsaccess->setdefaultfolderpermissions(permissions);
#ifdef ENABLE_SYNC
    client->syncs.setdefaultfolderpermissions(permissions);
#endif
}

int MegaApiImpl::getDefaultFolderPermissions()
{
    return fsAccess->getdefaultfolderpermissions();
}

long long MegaApiImpl::getBandwidthOverquotaDelay()
{
    long long result = client->overquotauntil;
    return result > Waiter::ds ? (result - Waiter::ds) / 10 : 0;
}

bool MegaApiImpl::userComparatorDefaultASC (User *i, User *j)
{
    if(strcasecmp(i->email.c_str(), j->email.c_str())<=0) return 1;
    return 0;
}

m_off_t MegaApiImpl::sizeDifference(Node* i, Node* j)
{
    assert(i->type == FOLDERNODE || i->size == i->getCounter().storage);
    assert(j->type == FOLDERNODE || j->size == j->getCounter().storage);
    return i->getCounter().storage - j->getCounter().storage;
}

char *MegaApiImpl::escapeFsIncompatible(const char *filename, const char *dstPath)
{
    if(!filename)
    {
        return NULL;
    }
    string name = filename;
    client->fsaccess->escapefsincompatible(&name,
        dstPath ? client->fsaccess->getlocalfstype(LocalPath::fromAbsolutePath(dstPath))
                : FS_UNKNOWN);
    return MegaApi::strdup(name.c_str());
}

char* MegaApiImpl::unescapeFsIncompatible(const char* name, const char* /*path*/)
{
    if (!name)
    {
        return NULL;
    }
    string filename = name;
    client->fsaccess->unescapefsincompatible(&filename);
    return MegaApi::strdup(filename.c_str());
}

bool MegaApiImpl::createThumbnail(const char *imagePath, const char *dstPath)
{
    if (!gfxAccess || !imagePath || !dstPath)
    {
        return false;
    }

    LocalPath localImagePath = LocalPath::fromAbsolutePath(imagePath);
    LocalPath localDstPath = LocalPath::fromAbsolutePath(dstPath);

    SdkMutexGuard g(sdkMutex);
    return gfxAccess->savefa(localImagePath, GfxProc::DIMENSIONS[GfxProc::THUMBNAIL], localDstPath);
}

bool MegaApiImpl::createPreview(const char *imagePath, const char *dstPath)
{
    if (!gfxAccess || !imagePath || !dstPath)
    {
        return false;
    }

    LocalPath localImagePath = LocalPath::fromAbsolutePath(imagePath);
    LocalPath localDstPath = LocalPath::fromAbsolutePath(dstPath);

    SdkMutexGuard g(sdkMutex);
    return gfxAccess->savefa(localImagePath, GfxProc::DIMENSIONS[GfxProc::PREVIEW], localDstPath);
}

bool MegaApiImpl::createAvatar(const char *imagePath, const char *dstPath)
{
    if (!gfxAccess || !imagePath || !dstPath)
    {
        return false;
    }

    LocalPath localImagePath = LocalPath::fromAbsolutePath(imagePath);
    LocalPath localDstPath = LocalPath::fromAbsolutePath(dstPath);

    SdkMutexGuard g(sdkMutex);
    return gfxAccess->savefa(localImagePath, GfxProc::DIMENSIONS_AVATAR[GfxProc::AVATAR250X250], localDstPath);
}

void MegaApiImpl::getUploadURL(int64_t fullFileSize, bool forceSSL, MegaRequestListener* listener)
{
    MegaRequestPrivate* req = new MegaRequestPrivate(MegaRequest::TYPE_GET_BACKGROUND_UPLOAD_URL, listener);
    req->setNumber(fullFileSize);
    req->setFlag(forceSSL);

    req->performRequest = [this, req]()
    {
        return performRequest_getBackgroundUploadURL(req);
    };

    requestQueue.push(req);
    waiter->notify();
}

void MegaApiImpl::completeUpload(const char* utf8Name, MegaNode *parent, const char* fingerprint, const char* fingerprintoriginal,
                                  const char *string64UploadToken, const char *string64FileKey,  MegaRequestListener *listener)
{
    MegaRequestPrivate* req = new MegaRequestPrivate(MegaRequest::TYPE_COMPLETE_BACKGROUND_UPLOAD, listener);
    req->setPassword(fingerprintoriginal);
    req->setNewPassword(fingerprint);
    req->setName(utf8Name);
    req->setPrivateKey(string64FileKey);
    if (parent)
    {
        req->setParentHandle(parent->getHandle());
    }
    if (string64UploadToken)
    {
        req->setSessionKey(string64UploadToken);
    }

    req->performRequest = [this, req]()
    {
        return performRequest_completeBackgroundUpload(req);
    };

    requestQueue.push(req);
    waiter->notify();
}


void MegaApiImpl::backgroundMediaUploadRequestUploadURL(int64_t fullFileSize, MegaBackgroundMediaUpload* state, MegaRequestListener *listener)
{
    MegaRequestPrivate* req = new MegaRequestPrivate(MegaRequest::TYPE_GET_BACKGROUND_UPLOAD_URL, listener);
    req->setNumber(fullFileSize);
    req->setFlag(true); // always SSL for background uploads
    req->setMegaBackgroundMediaUploadPtr(state);

    req->performRequest = [this, req]()
    {
        return performRequest_getBackgroundUploadURL(req);
    };

    requestQueue.push(req);
    waiter->notify();
}

void MegaApiImpl::backgroundMediaUploadComplete(MegaBackgroundMediaUpload* state, const char* utf8Name, MegaNode *parent, const char* fingerprint, const char* fingerprintoriginal,
    const char *string64UploadToken, MegaRequestListener *listener)
{
    MegaRequestPrivate* req = new MegaRequestPrivate(MegaRequest::TYPE_COMPLETE_BACKGROUND_UPLOAD, listener);
    req->setMegaBackgroundMediaUploadPtr(static_cast<MegaBackgroundMediaUploadPrivate*>(state));
    req->setPassword(fingerprintoriginal);
    req->setNewPassword(fingerprint);
    req->setName(utf8Name);
    if (parent)
    {
        req->setParentHandle(parent->getHandle());
    }
    if (string64UploadToken)
    {
        req->setSessionKey(string64UploadToken);
    }

    req->performRequest = [this, req]()
    {
        return performRequest_completeBackgroundUpload(req);
    };

    requestQueue.push(req);
    waiter->notify();
}

bool MegaApiImpl::ensureMediaInfo()
{
#ifdef USE_MEDIAINFO
    if (client->mediaFileInfo.mediaCodecsReceived)
    {
        return true;
    }
    else
    {
        SdkMutexGuard g(sdkMutex);
        client->mediaFileInfo.requestCodecMappingsOneTime(client, LocalPath());
        return false;
    }
#else
    return false;
#endif
}

void MegaApiImpl::setOriginalFingerprint(MegaNode* node, const char* originalFingerprint, MegaRequestListener *listener)
{
    MegaRequestPrivate* req = new MegaRequestPrivate(MegaRequest::TYPE_SET_ATTR_NODE, listener);
    req->setParamType(MegaApi::NODE_ATTR_ORIGINALFINGERPRINT);
    req->setText(originalFingerprint);
    req->setNodeHandle(node->getHandle());
    req->setFlag(true);

    req->performRequest = [this, req]()
    {
        return performRequest_setAttrNode(req);
    };

    requestQueue.push(req);
    waiter->notify();
}

bool MegaApiImpl::isOnline()
{
    return !client->httpio->noinetds;
}

#ifdef HAVE_LIBUV
bool MegaApiImpl::httpServerStart(bool localOnly, int port, bool useTLS, const char *certificatepath, const char *keypath, bool useIPv6)
{
    #ifndef ENABLE_EVT_TLS
    if (useTLS)
    {
        LOG_err << "Could not start HTTP server: TLS is not supported in current compilation";
        return false;
    }
    #endif

    if (useTLS && (!certificatepath || !keypath || !strlen(certificatepath) || !strlen(keypath)))
    {
        LOG_err << "Could not start HTTP server: No certificate/key provided";
        return false;
    }

    SdkMutexGuard g(sdkMutex);
    if (httpServer && httpServer->getPort() == port && httpServer->isLocalOnly() == localOnly)
    {
        httpServer->clearAllowedHandles();
        return true;
    }

    httpServerStop();
    httpServer = new MegaHTTPServer(this, basePath, useTLS, certificatepath ? certificatepath : string(), keypath ? keypath : string(), useIPv6);
    httpServer->setMaxBufferSize(httpServerMaxBufferSize);
    httpServer->setMaxOutputSize(httpServerMaxOutputSize);
    httpServer->enableFileServer(httpServerEnableFiles);
    httpServer->enableOfflineAttribute(httpServerOfflineAttributeEnabled);
    httpServer->enableFolderServer(httpServerEnableFolders);
    httpServer->setRestrictedMode(httpServerRestrictedMode);
    httpServer->enableSubtitlesSupport(httpServerRestrictedMode != 0);

    bool result = httpServer->start(port, localOnly);
    if (!result)
    {
        MegaHTTPServer *server = httpServer;
        httpServer = NULL;
        g.unlock();
        delete server;
    }
    return result;
}

void MegaApiImpl::httpServerStop()
{
    SdkMutexGuard g(sdkMutex);
    if (httpServer)
    {
        MegaHTTPServer *server = httpServer;
        httpServer = NULL;
        g.unlock();
        server->stop();
        delete server;
    }
}

int MegaApiImpl::httpServerIsRunning()
{
    SdkMutexGuard g(sdkMutex);
    if (httpServer)
    {
        return httpServer->getPort();
    }
    return 0;
}

char *MegaApiImpl::httpServerGetLocalLink(MegaNode *node)
{
    if (!node)
    {
        return NULL;
    }

    SdkMutexGuard g(sdkMutex);
    if (!httpServer)
    {
        return NULL;
    }

    return httpServer->getLink(node, "http");
}

char *MegaApiImpl::httpServerGetLocalWebDavLink(MegaNode *node)
{
    if (!node)
    {
        return NULL;
    }

    SdkMutexGuard g(sdkMutex);
    if (!httpServer)
    {
        return NULL;
    }

    return httpServer->getWebDavLink(node);
}

MegaStringList *MegaApiImpl::httpServerGetWebDavLinks()
{
    SdkMutexGuard g(sdkMutex);
    if (!httpServer)
    {
        return NULL;
    }

    set<handle> handles = httpServer->getAllowedWebDavHandles();

    string_vector listoflinks;

    for (std::set<handle>::iterator it = handles.begin(); it != handles.end(); ++it)
    {
        handle h = *it;
        MegaNode *n = getNodeByHandle(h);
        if (n)
        {
            unique_ptr<char[]> link(httpServer->getWebDavLink(n));
            listoflinks.push_back(link.get());
        }
    }

    return new MegaStringListPrivate(std::move(listoflinks));
}

MegaNodeList *MegaApiImpl::httpServerGetWebDavAllowedNodes()
{
    SdkMutexGuard g(sdkMutex);
    if (!httpServer)
    {
        return NULL;
    }

    set<handle> handles = httpServer->getAllowedWebDavHandles();

    vector<std::shared_ptr<Node>> listofnodes;

    for (std::set<handle>::iterator it = handles.begin(); it != handles.end(); ++it)
    {
        handle h = *it;
        std::shared_ptr<Node>n = client->nodebyhandle(h);
        if (n)
        {
            listofnodes.push_back(n);
        }
    }

    return new MegaNodeListPrivate(listofnodes);
}

void MegaApiImpl::httpServerRemoveWebDavAllowedNode(MegaHandle handle)
{
    SdkMutexGuard g(sdkMutex);
    if (httpServer)
    {
        httpServer->removeAllowedWebDavHandle(handle);
    }
}

void MegaApiImpl::httpServerRemoveWebDavAllowedNodes()
{
    SdkMutexGuard g(sdkMutex);
    if (httpServer)
    {
        httpServer->clearAllowedHandles();
    }
}

void MegaApiImpl::httpServerSetMaxBufferSize(int bufferSize)
{
    SdkMutexGuard g(sdkMutex);
    httpServerMaxBufferSize = bufferSize <= 0 ? 0 : bufferSize;
    httpServerMaxOutputSize = httpServerMaxBufferSize / 10;
    if (httpServer)
    {
        httpServer->setMaxBufferSize(httpServerMaxBufferSize);
        httpServer->setMaxOutputSize(httpServerMaxOutputSize);
    }
}

int MegaApiImpl::httpServerGetMaxBufferSize()
{
    SdkMutexGuard g(sdkMutex);
    if (httpServerMaxBufferSize)
    {
        return httpServerMaxBufferSize;
    }
    else
    {
        return StreamingBuffer::MAX_BUFFER_SIZE;
    }
}

void MegaApiImpl::httpServerSetMaxOutputSize(int outputSize)
{
    SdkMutexGuard g(sdkMutex);
    httpServerMaxOutputSize = outputSize <= 0 ? 0 : outputSize;
    if (httpServer)
    {
        httpServer->setMaxOutputSize(httpServerMaxOutputSize);
    }
}

int MegaApiImpl::httpServerGetMaxOutputSize()
{
    SdkMutexGuard g(sdkMutex);
    if (httpServerMaxOutputSize)
    {
        return httpServerMaxOutputSize;
    }
    else
    {
        return StreamingBuffer::MAX_OUTPUT_SIZE;
    }
}

void MegaApiImpl::httpServerSetThrottleBitrate(unsigned long long bitrateBps)
{
    LOG_debug << "[Throttle] httpServerSetThrottleBitrate: " << httpServerThrottleBitrateBps
              << " -> " << bitrateBps << " bps"
              << " (" << (bitrateBps / 8 / 1024 / 1024) << " MB/s)";

    httpServerThrottleBitrateBps = bitrateBps;
}

unsigned long long MegaApiImpl::httpServerGetThrottleBitrate()
{
    return httpServerThrottleBitrateBps;
}

void MegaApiImpl::httpServerEnableFileServer(bool enable)
{
    SdkMutexGuard g(sdkMutex);
    this->httpServerEnableFiles = enable;
    if (httpServer)
    {
        httpServer->enableFileServer(enable);
    }
}

bool MegaApiImpl::httpServerIsFileServerEnabled()
{
    return httpServerEnableFiles;
}

void MegaApiImpl::httpServerEnableFolderServer(bool enable)
{
    SdkMutexGuard g(sdkMutex);
    this->httpServerEnableFolders = enable;
    if (httpServer)
    {
        httpServer->enableFolderServer(enable);
    }
}

void MegaApiImpl::httpServerEnableOfflineAttribute(bool enable)
{
    SdkMutexGuard g(sdkMutex);
    this->httpServerOfflineAttributeEnabled = enable;
    if (httpServer)
    {
        httpServer->enableOfflineAttribute(enable);
    }
}

bool MegaApiImpl::httpServerIsFolderServerEnabled()
{
    return httpServerEnableFolders;
}

bool MegaApiImpl::httpServerIsOfflineAttributeEnabled()
{
    return httpServerOfflineAttributeEnabled;
}

void MegaApiImpl::httpServerSetRestrictedMode(int mode)
{
    if (mode != MegaApi::TCP_SERVER_DENY_ALL
            && mode != MegaApi::TCP_SERVER_ALLOW_ALL
            && mode != MegaApi::TCP_SERVER_ALLOW_CREATED_LOCAL_LINKS
            && mode != MegaApi::TCP_SERVER_ALLOW_LAST_LOCAL_LINK)
    {
        return;
    }

    SdkMutexGuard g(sdkMutex);
    httpServerRestrictedMode = mode;
    if (httpServer)
    {
        httpServer->setRestrictedMode(httpServerRestrictedMode);
    }
}

int MegaApiImpl::httpServerGetRestrictedMode()
{
    return httpServerRestrictedMode;
}

void MegaApiImpl::httpServerEnableSubtitlesSupport(bool enable)
{
    SdkMutexGuard g(sdkMutex);
    httpServerSubtitlesSupportEnabled = enable;
    if (httpServer)
    {
        httpServer->enableSubtitlesSupport(httpServerSubtitlesSupportEnabled);
    }
}

bool MegaApiImpl::httpServerIsSubtitlesSupportEnabled()
{
    return httpServerSubtitlesSupportEnabled;
}

bool MegaApiImpl::httpServerIsLocalOnly()
{
    bool localOnly = true;
    SdkMutexGuard g(sdkMutex);
    if (httpServer)
    {
        localOnly = httpServer->isLocalOnly();
    }
    return localOnly;
}

bool MegaApiImpl::ftpServerStart(bool localOnly, int port, int dataportBegin, int dataPortEnd, bool useTLS, const char *certificatepath, const char *keypath)
{
    #ifndef ENABLE_EVT_TLS
    if (useTLS)
    {
        LOG_err << "Could not start FTP server: TLS is not supported in current compilation";
        return false;
    }
    #endif

    SdkMutexGuard g(sdkMutex);
    if (ftpServer && ftpServer->getPort() == port && ftpServer->isLocalOnly() == localOnly)
    {
        ftpServer->clearAllowedHandles();
        return true;
    }

    ftpServerStop();
    ftpServer = new MegaFTPServer(this, basePath, dataportBegin, dataPortEnd, useTLS, certificatepath ? certificatepath : string(), keypath ? keypath : string());
    ftpServer->setRestrictedMode(MegaApi::TCP_SERVER_ALLOW_CREATED_LOCAL_LINKS);
    ftpServer->setRestrictedMode(ftpServerRestrictedMode);
    ftpServer->setMaxBufferSize(ftpServerMaxBufferSize);
    ftpServer->setMaxOutputSize(ftpServerMaxOutputSize);

    bool result = ftpServer->start(port, localOnly);
    if (!result)
    {
        MegaFTPServer *server = ftpServer;
        ftpServer = NULL;
        g.unlock();
        delete server;
    }
    return result;
}

void MegaApiImpl::ftpServerStop()
{
    SdkMutexGuard g(sdkMutex);
    if (ftpServer)
    {
        MegaFTPServer *server = ftpServer;
        ftpServer = NULL;
        g.unlock();
        server->stop();
        delete server;
    }
}

int MegaApiImpl::ftpServerIsRunning()
{
    SdkMutexGuard g(sdkMutex);
    if (ftpServer)
    {
        return ftpServer->getPort();
    }
    return 0;
}

char *MegaApiImpl::ftpServerGetLocalLink(MegaNode *node)
{
    if (!node)
    {
        return NULL;
    }

    SdkMutexGuard g(sdkMutex);
    if (!ftpServer)
    {
        return NULL;
    }

    return ftpServer->getLink(node, "ftp");
}

MegaStringList *MegaApiImpl::ftpServerGetLinks()
{
    SdkMutexGuard g(sdkMutex);
    if (!ftpServer)
    {
        return NULL;
    }

    set<handle> handles = ftpServer->getAllowedHandles();

    string_vector listoflinks;

    for (std::set<handle>::iterator it = handles.begin(); it != handles.end(); ++it)
    {
        handle h = *it;
        MegaNode *n = getNodeByHandle(h);
        if (n)
        {
            unique_ptr<char[]> link(ftpServer->getLink(n));
            listoflinks.push_back(link.get());
        }
    }

    return new MegaStringListPrivate(std::move(listoflinks));
}

MegaNodeList *MegaApiImpl::ftpServerGetAllowedNodes()
{
    SdkMutexGuard g(sdkMutex);
    if (!ftpServer)
    {
        return NULL;
    }

    set<handle> handles = ftpServer->getAllowedHandles();

    vector<std::shared_ptr<Node>> listofnodes;

    for (std::set<handle>::iterator it = handles.begin(); it != handles.end(); ++it)
    {
        handle h = *it;
        std::shared_ptr<Node>n = client->nodebyhandle(h);
        if (n)
        {
            listofnodes.push_back(n);
        }
    }

    return new MegaNodeListPrivate(listofnodes);
}

void MegaApiImpl::ftpServerRemoveAllowedNode(MegaHandle handle)
{
    SdkMutexGuard g(sdkMutex);
    if (ftpServer)
    {
        ftpServer->removeAllowedHandle(handle);
    }
}

void MegaApiImpl::ftpServerRemoveAllowedNodes()
{
    SdkMutexGuard g(sdkMutex);
    if (ftpServer)
    {
        ftpServer->clearAllowedHandles();
    }
}

void MegaApiImpl::ftpServerSetMaxBufferSize(int bufferSize)
{
    SdkMutexGuard g(sdkMutex);
    ftpServerMaxBufferSize = bufferSize <= 0 ? 0 : bufferSize;
    if (ftpServer)
    {
        ftpServer->setMaxBufferSize(ftpServerMaxBufferSize);
    }
}

int MegaApiImpl::ftpServerGetMaxBufferSize()
{
    SdkMutexGuard g(sdkMutex);
    if (ftpServerMaxBufferSize)
    {
        return ftpServerMaxBufferSize;
    }
    else
    {
        return StreamingBuffer::MAX_BUFFER_SIZE;
    }
}

void MegaApiImpl::ftpServerSetMaxOutputSize(int outputSize)
{
    SdkMutexGuard g(sdkMutex);
    ftpServerMaxOutputSize = outputSize <= 0 ? 0 : outputSize;
    if (ftpServer)
    {
        ftpServer->setMaxOutputSize(ftpServerMaxOutputSize);
    }
}

int MegaApiImpl::ftpServerGetMaxOutputSize()
{
    SdkMutexGuard g(sdkMutex);
    if (ftpServerMaxOutputSize)
    {
        return ftpServerMaxOutputSize;
    }
    else
    {
        return StreamingBuffer::MAX_OUTPUT_SIZE;
    }
}

void MegaApiImpl::ftpServerSetRestrictedMode(int mode)
{
    if (mode != MegaApi::TCP_SERVER_DENY_ALL
            && mode != MegaApi::TCP_SERVER_ALLOW_ALL
            && mode != MegaApi::TCP_SERVER_ALLOW_CREATED_LOCAL_LINKS
            && mode != MegaApi::TCP_SERVER_ALLOW_LAST_LOCAL_LINK)
    {
        return;
    }

    SdkMutexGuard g(sdkMutex);
    ftpServerRestrictedMode = mode;
    if (ftpServer)
    {
        ftpServer->setRestrictedMode(ftpServerRestrictedMode);
    }
}

int MegaApiImpl::ftpServerGetRestrictedMode()
{
    return ftpServerRestrictedMode;
}

bool MegaApiImpl::ftpServerIsLocalOnly()
{
    bool localOnly = true;
    SdkMutexGuard g(sdkMutex);
    if (ftpServer)
    {
        localOnly = ftpServer->isLocalOnly();
    }
    return localOnly;
}

void MegaApiImpl::ftpServerAddListener(MegaTransferListener *listener)
{
    if (!listener)
    {
        return;
    }

    SdkMutexGuard g(sdkMutex);
    ftpServerListeners.insert(listener);
}

void MegaApiImpl::ftpServerRemoveListener(MegaTransferListener *listener)
{
    if (!listener)
    {
        return;
    }

    SdkMutexGuard g(sdkMutex);
    ftpServerListeners.erase(listener);
}

void MegaApiImpl::fireOnFtpStreamingStart(MegaTransferPrivate *transfer)
{
    assert(threadId == std::this_thread::get_id());
    for(set<MegaTransferListener *>::iterator it = ftpServerListeners.begin(); it != ftpServerListeners.end() ; it++)
        (*it)->onTransferStart(api, transfer);
}

void MegaApiImpl::fireOnFtpStreamingTemporaryError(MegaTransferPrivate *transfer, unique_ptr<MegaErrorPrivate> e)
{
    assert(threadId == std::this_thread::get_id());
    for(set<MegaTransferListener *>::iterator it = ftpServerListeners.begin(); it != ftpServerListeners.end() ; it++)
        (*it)->onTransferTemporaryError(api, transfer, e.get());
}

void MegaApiImpl::fireOnFtpStreamingFinish(MegaTransferPrivate *transfer, unique_ptr<MegaErrorPrivate> e)
{
    assert(threadId == std::this_thread::get_id());
    if(e->getErrorCode())
    {
        LOG_warn << "Streaming request finished with error: " << e->getErrorString();
    }
    else
    {
        LOG_info << "Streaming request finished";
    }

    for(set<MegaTransferListener *>::iterator it = ftpServerListeners.begin(); it != ftpServerListeners.end() ; it++)
        (*it)->onTransferFinish(api, transfer, e.get());

    delete transfer;
}

#endif

#ifdef ENABLE_CHAT
void MegaApiImpl::sendChatStats(const char *data, int port, MegaRequestListener *listener)
{
    MegaRequestPrivate *request = new MegaRequestPrivate(MegaRequest::TYPE_CHAT_STATS, listener);
    request->setName(data);
    request->setNumber(port);
    request->setParamType(1);

    request->performRequest = [this, request]()
    {
        return performRequest_chatStats(request);
    };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::sendChatLogs(const char *data, MegaHandle userid, MegaHandle callid, int port, MegaRequestListener *listener)
{
    MegaRequestPrivate *request = new MegaRequestPrivate(MegaRequest::TYPE_CHAT_STATS, listener);
    request->setName(data);
    request->setNodeHandle(userid);
    request->setParentHandle(callid);
    request->setParamType(2);
    request->setNumber(port);

    request->performRequest = [this, request]()
    {
        return performRequest_chatStats(request);
    };

    requestQueue.push(request);
    waiter->notify();
}

MegaTextChatList *MegaApiImpl::getChatList()
{
    SdkMutexGuard g(sdkMutex);
    return new MegaTextChatListPrivate(&client->chats);
}

MegaHandleList *MegaApiImpl::getAttachmentAccess(MegaHandle chatid, MegaHandle h)
{
    MegaHandleList *uhList = new MegaHandleListPrivate();

    if (chatid == INVALID_HANDLE || h == INVALID_HANDLE)
    {
        return uhList;
    }

    SdkMutexGuard g(sdkMutex);

    textchat_map::iterator itc = client->chats.find(chatid);
    if (itc != client->chats.end())
    {
        set<handle> userList = itc->second->getUsersOfAttachment(h);
        std::for_each(userList.begin(), userList.end(), [uhList](const handle& u) { uhList->addMegaHandle(u); });
    }
    return uhList;
}

bool MegaApiImpl::hasAccessToAttachment(MegaHandle chatid, MegaHandle h, MegaHandle uh)
{
    bool ret = false;

    if (chatid == INVALID_HANDLE || h == INVALID_HANDLE || uh == INVALID_HANDLE)
    {
        return ret;
    }

    SdkMutexGuard g(sdkMutex);

    textchat_map::iterator itc = client->chats.find(chatid);
    if (itc != client->chats.end())
    {
        ret = itc->second->isUserOfAttachment(h, uh);
    }
    return ret;
}

const char* MegaApiImpl::getFileAttribute(MegaHandle h)
{
    char* fileAttributes = NULL;

    SdkMutexGuard g(sdkMutex);
    if (std::shared_ptr<Node> node = client->nodebyhandle(h))
    {
        fileAttributes = MegaApi::strdup(node->fileattrstring.c_str());
    }
    return fileAttributes;
}

void MegaApiImpl::enableRichPreviews(bool enable, MegaRequestListener *listener)
{
    MegaStringMap *stringMap = new MegaStringMapPrivate();
    string rawvalue = enable ? "1" : "0";
    string base64value;
    Base64::btoa(rawvalue, base64value);
    stringMap->set("num", base64value.c_str());
    setUserAttr(MegaApi::USER_ATTR_RICH_PREVIEWS, stringMap, listener);
    delete stringMap;
}

void MegaApiImpl::isRichPreviewsEnabled(MegaRequestListener *listener)
{
    MegaRequestPrivate *request = new MegaRequestPrivate(MegaRequest::TYPE_GET_ATTR_USER, listener);
    request->setParamType(MegaApi::USER_ATTR_RICH_PREVIEWS);
    request->setNumDetails(0);  // 0 --> flag should indicate whether rich-links are enabled or not

    request->performRequest = [this, request]()
    {
        return performRequest_getAttrUser(request);
    };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::shouldShowRichLinkWarning(MegaRequestListener *listener)
{
    MegaRequestPrivate *request = new MegaRequestPrivate(MegaRequest::TYPE_GET_ATTR_USER, listener);
    request->setParamType(MegaApi::USER_ATTR_RICH_PREVIEWS);
    request->setNumDetails(1);  // 1 --> flag should indicate whether to show the warning or not

    request->performRequest = [this, request]()
    {
        return performRequest_getAttrUser(request);
    };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::setRichLinkWarningCounterValue(int value, MegaRequestListener *listener)
{
    MegaStringMap *stringMap = new MegaStringMapPrivate();
    std::ostringstream oss;
    oss << value;
    string base64value;
    Base64::btoa(oss.str(), base64value);
    stringMap->set("c", base64value.c_str());
    setUserAttr(MegaApi::USER_ATTR_RICH_PREVIEWS, stringMap, listener);
    delete stringMap;
}

void MegaApiImpl::enableGeolocation(MegaRequestListener *listener)
{
    MegaStringMap *stringMap = new MegaStringMapPrivate();
    string base64value;
    Base64::btoa("1", base64value);
    stringMap->set("v", base64value.c_str());
    setUserAttr(MegaApi::USER_ATTR_GEOLOCATION, stringMap, listener);
    delete stringMap;
}

void MegaApiImpl::isGeolocationEnabled(MegaRequestListener *listener)
{
    MegaRequestPrivate *request = new MegaRequestPrivate(MegaRequest::TYPE_GET_ATTR_USER, listener);
    request->setParamType(MegaApi::USER_ATTR_GEOLOCATION);

    request->performRequest = [this, request]()
    {
        return performRequest_getAttrUser(request);
    };

    requestQueue.push(request);
    waiter->notify();
}

bool MegaApiImpl::isChatNotifiable(MegaHandle chatid)
{
    std::unique_ptr<MegaPushNotificationSettingsPrivate> pushSettings = getMegaPushNotificationSetting();
    if (pushSettings)
    {
        if (pushSettings->isChatAlwaysNotifyEnabled(chatid))
        {
            return true;
        }

        return (!pushSettings->isChatDndEnabled(chatid) && isGlobalNotifiable(pushSettings.get()) && !pushSettings->isGlobalChatsDndEnabled());
    }

    return true;
}

void MegaApiImpl::setSFUid(int sfuid)
{
    SdkMutexGuard g(sdkMutex);
    client->mSfuid = sfuid;
}
#endif

void MegaApiImpl::getCameraUploadsFolder(bool secondary, MegaRequestListener *listener)
{
    MegaRequestPrivate *request = new MegaRequestPrivate(MegaRequest::TYPE_GET_ATTR_USER, listener);
    request->setParamType(MegaApi::USER_ATTR_CAMERA_UPLOADS_FOLDER);
    request->setFlag(secondary);

    request->performRequest = [this, request]()
    {
        return performRequest_getAttrUser(request);
    };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::setCameraUploadsFolder(MegaHandle nodehandle, bool secondary, MegaRequestListener *listener)
{
    MegaRequestPrivate *request = new MegaRequestPrivate(MegaRequest::TYPE_SET_ATTR_USER, listener);

    MegaStringMapPrivate stringMap;
    const char *key = secondary ? "sh" : "h";
    stringMap.set(key, Base64Str<MegaClient::NODEHANDLE>(nodehandle));
    request->setMegaStringMap(&stringMap);
    request->setParamType(MegaApi::USER_ATTR_CAMERA_UPLOADS_FOLDER);
    request->setFlag(secondary);
    request->setNodeHandle(nodehandle);

    request->performRequest = [this, request]()
    {
        return performRequest_setAttrUser(request);
    };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::setCameraUploadsFolders(MegaHandle primaryFolder, MegaHandle secondaryFolder, MegaRequestListener *listener)
{
    MegaRequestPrivate *request = new MegaRequestPrivate(MegaRequest::TYPE_SET_ATTR_USER, listener);

    MegaStringMapPrivate stringMap;
    if (!ISUNDEF(primaryFolder))
    {
        stringMap.set("h", Base64Str<MegaClient::NODEHANDLE>(primaryFolder));
    }
    if (!ISUNDEF(secondaryFolder))
    {
        stringMap.set("sh", Base64Str<MegaClient::NODEHANDLE>(secondaryFolder));
    }
    request->setMegaStringMap(&stringMap);
    request->setParamType(MegaApi::USER_ATTR_CAMERA_UPLOADS_FOLDER);
    request->setNodeHandle(primaryFolder);
    request->setParentHandle(secondaryFolder);

    request->performRequest = [this, request]()
    {
        return performRequest_setAttrUser(request);
    };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::getMyChatFilesFolder(MegaRequestListener *listener)
{
    MegaRequestPrivate *request = new MegaRequestPrivate(MegaRequest::TYPE_GET_ATTR_USER, listener);
    request->setParamType(MegaApi::USER_ATTR_MY_CHAT_FILES_FOLDER);

    request->performRequest = [this, request]()
    {
        return performRequest_getAttrUser(request);
    };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::setMyChatFilesFolder(MegaHandle nodehandle, MegaRequestListener *listener)
{
    MegaRequestPrivate *request = new MegaRequestPrivate(MegaRequest::TYPE_SET_ATTR_USER, listener);

    MegaStringMapPrivate stringMap;
    stringMap.set("h", Base64Str<MegaClient::NODEHANDLE>(nodehandle));
    request->setMegaStringMap(&stringMap);
    request->setParamType(MegaApi::USER_ATTR_MY_CHAT_FILES_FOLDER);
    request->setNodeHandle(nodehandle);

    request->performRequest = [this, request]()
    {
        return performRequest_setAttrUser(request);
    };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::getUserAlias(MegaHandle uh, MegaRequestListener *listener)
{
    MegaRequestPrivate *request = new MegaRequestPrivate(MegaRequest::TYPE_GET_ATTR_USER, listener);
    request->setParamType(MegaApi::USER_ATTR_ALIAS);
    request->setNodeHandle(uh);
    request->setText(Base64Str<MegaClient::USERHANDLE>(uh));

    request->performRequest = [this, request]()
    {
        return performRequest_getAttrUser(request);
    };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::setUserAlias(MegaHandle uh, const char *alias, MegaRequestListener *listener)
{
    MegaRequestPrivate *request = new MegaRequestPrivate(MegaRequest::TYPE_SET_ATTR_USER, listener);
    MegaStringMapPrivate stringMap;
    string buf = alias ? alias : "";    // alias is null to remove it
    stringMap.set(Base64Str<MegaClient::USERHANDLE>(uh), Base64::btoa(buf).c_str());
    request->setMegaStringMap(&stringMap);
    request->setParamType(MegaApi::USER_ATTR_ALIAS);
    request->setNodeHandle(uh);
    request->setText(alias);

    request->performRequest = [this, request]()
    {
        return performRequest_setAttrUser(request);
    };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::getPushNotificationSettings(MegaRequestListener *listener)
{
    MegaRequestPrivate *request = new MegaRequestPrivate(MegaRequest::TYPE_GET_ATTR_USER, listener);
    request->setParamType(MegaApi::USER_ATTR_PUSH_SETTINGS);

    request->performRequest = [this, request]()
    {
        return performRequest_getAttrUser(request);
    };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::setPushNotificationSettings(MegaPushNotificationSettings *settings, MegaRequestListener *listener)
{
    MegaRequestPrivate *request = new MegaRequestPrivate(MegaRequest::TYPE_SET_ATTR_USER, listener);
    request->setParamType(MegaApi::USER_ATTR_PUSH_SETTINGS);
    request->setMegaPushNotificationSettings(settings);

    request->performRequest = [this, request]()
    {
        return performRequest_setAttrUser(request);
    };

    requestQueue.push(request);
    waiter->notify();
}

bool MegaApiImpl::isSharesNotifiable()
{
    std::unique_ptr<MegaPushNotificationSettingsPrivate> pushSettings = getMegaPushNotificationSetting();
    return !pushSettings || (pushSettings->isSharesEnabled() && isScheduleNotifiable(pushSettings.get()));
}

bool MegaApiImpl::isContactsNotifiable()
{
    std::unique_ptr<MegaPushNotificationSettingsPrivate> pushSettings = getMegaPushNotificationSetting();
    return !pushSettings || (pushSettings->isContactsEnabled() && isScheduleNotifiable(pushSettings.get()));
}

void MegaApiImpl::getAccountAchievements(MegaRequestListener *listener)
{
    MegaRequestPrivate *request = new MegaRequestPrivate(MegaRequest::TYPE_GET_ACHIEVEMENTS, listener);

    request->performRequest = [this, request]()
    {
        return performRequest_getAchievements(request);
    };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::getMegaAchievements(MegaRequestListener *listener)
{
    MegaRequestPrivate *request = new MegaRequestPrivate(MegaRequest::TYPE_GET_ACHIEVEMENTS, listener);
    request->setFlag(true);

    request->performRequest = [this, request]()
    {
        return performRequest_getAchievements(request);
    };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::catchup(MegaRequestListener *listener)
{
    MegaRequestPrivate *request = new MegaRequestPrivate(MegaRequest::TYPE_CATCHUP, listener);
    scRequestQueue.push(request);
    waiter->notify();
}

MegaUserList* MegaApiImpl::getContacts()
{
    SdkMutexGuard g(sdkMutex);

    vector<User*> vUsers;
    for (user_map::iterator it = client->users.begin() ; it != client->users.end() ; it++ )
    {
        User *u = &(it->second);
        if (u->userhandle == client->me)
        {
            continue;
        }
        vector<User *>::iterator i = std::lower_bound(vUsers.begin(), vUsers.end(), u, MegaApiImpl::userComparatorDefaultASC);
        vUsers.insert(i, u);
    }
    return new MegaUserListPrivate(vUsers.data(), int(vUsers.size()));
}


MegaUser* MegaApiImpl::getContact(const char *uid)
{
    SdkMutexGuard g(sdkMutex);
    MegaUser *user = MegaUserPrivate::fromUser(client->finduser(uid, 0));

    if (user && user->getHandle() == client->me)
    {
        delete user;
        user = NULL;    // it's not a contact
    }

    return user;
}

MegaUserAlertList* MegaApiImpl::getUserAlerts()
{
    SdkMutexGuard g(sdkMutex);

    vector<UserAlert::Base*> v;
    v.reserve(client->useralerts.alerts.size());
    for (UserAlerts::Alerts::iterator it = client->useralerts.alerts.begin(); it != client->useralerts.alerts.end(); ++it)
    {
        if (!(*it)->removed())
        {
            v.push_back(*it);
        }
    }
    return new MegaUserAlertListPrivate(v.data(), int(v.size()), client);
}

int MegaApiImpl::getNumUnreadUserAlerts()
{
    int result = 0;
    SdkMutexGuard g(sdkMutex);
    for (UserAlerts::Alerts::iterator it = client->useralerts.alerts.begin(); it != client->useralerts.alerts.end(); ++it)
    {
        if (!(*it)->removed() && !(*it)->seen())
        {
            result++;
        }
    }
    return result;
}

MegaNodeList* MegaApiImpl::getInShares(MegaUser *megaUser, int order)
{
    if (!megaUser)
    {
        return new MegaNodeListPrivate();
    }

    SdkMutexGuard g(sdkMutex);
    sharedNode_vector vNodes;
    User *user = client->finduser(megaUser->getEmail(), 0);
    if (!user)
    {
        return new MegaNodeListPrivate();
    }

    for (handle_set::iterator sit = user->sharing.begin(); sit != user->sharing.end(); sit++)
    {
        std::shared_ptr<Node> n;
        if ((n = client->nodebyhandle(*sit)) && !n->parent)
        {
            vNodes.push_back(n);
        }
    }

    MegaNodeList *nodeList;
    if (vNodes.size())
    {
        sortByComparatorFunction(vNodes, order, *client);
        nodeList = new MegaNodeListPrivate(vNodes);
    }
    else
    {
        nodeList = new MegaNodeListPrivate();
    }
    return nodeList;
}

MegaNodeList* MegaApiImpl::getInShares(int order)
{
    SdkMutexGuard lock(sdkMutex);

    sharedNode_vector sharedNodes = client->getInShares();

    sortByComparatorFunction(sharedNodes, order, *client);

    return new MegaNodeListPrivate(sharedNodes);
}

MegaShareList* MegaApiImpl::getInSharesList(int order)
{
    SdkMutexGuard lock(sdkMutex);

    sharedNode_vector nodes = client->getVerifiedInShares();

    sortByComparatorFunction(nodes, order, *client);

    vector<impl::ShareData> shares;
    for (const auto& node: nodes)
    {
        shares.emplace_back(node->nodehandle, node->inshare.get(), true);
    }

    return new MegaShareListPrivate(shares);
}

MegaUser *MegaApiImpl::getUserFromInShare(MegaNode *megaNode, bool recurse)
{
    if (!megaNode)
    {
        return NULL;
    }

    MegaUser *user = NULL;

    SdkMutexGuard g(sdkMutex);

    std::shared_ptr<Node> node = client->nodebyhandle(megaNode->getHandle());
    if (recurse && node)
    {
        node = client->getrootnode(node);
    }

    if (node && node->inshare && node->inshare->user)
    {
        user = MegaUserPrivate::fromUser(node->inshare->user);
    }
    return user;
}

bool MegaApiImpl::isPendingShare(MegaNode *megaNode)
{
    if(!megaNode) return false;

    SdkMutexGuard g(sdkMutex);
    std::shared_ptr<Node> node = client->nodebyhandle(megaNode->getHandle());
    if(!node)
    {
        return false;
    }

    return node->pendingshares != NULL;
}

//
// Retrieve nodes that have any outgoing shares (including pending ones). Each node appears
// only once in the list.
//
sharedNode_vector MegaApiImpl::getSharedNodes() const
{
    sharedNode_vector outshares = client->mNodeManager.getNodesWithOutShares();

    // Avoid duplicate nodes present in both outshares and pending shares
    sharedNode_vector pendingShares = client->mNodeManager.getNodesWithPendingOutShares();
    for (auto& pendingShare : pendingShares)
    {
        bool found = false;
        for (auto& node : outshares)
        {
            if (node->nodeHandle() == pendingShare->nodeHandle())
            {
                found = true;
                break;
            }
        }

        if (!found)
        {
            outshares.push_back(pendingShare);
        }
    }

    return outshares;
}

MegaShareList* MegaApiImpl::getOutShares(int order)
{
    SdkMutexGuard guard(sdkMutex);

    // Get nodes having any outgoing shares
    sharedNode_vector sharedNodes = getSharedNodes();

    // Sort nodes in place
    MegaApiImpl::sortByComparatorFunction(sharedNodes, order, *client);

    // Extract shares from nodes
    auto shares = impl::ShareExtractor::extractOutShares(sharedNodes, client->mKeyManager);

    // Sort shares in place
    impl::ShareSorter::sort(shares, order);

    return new MegaShareListPrivate(shares);
}

MegaShareList* MegaApiImpl::getOutShares(MegaNode *megaNode)
{
    if(!megaNode) return new MegaShareListPrivate();

    SdkMutexGuard g(sdkMutex);
    std::shared_ptr<Node> node = client->nodebyhandle(megaNode->getHandle());
    if(!node)
    {
        return new MegaShareListPrivate();
    }

    return new MegaShareListPrivate(
        impl::ShareExtractor::extractOutShares({node}, client->mKeyManager));
}

MegaShareList *MegaApiImpl::getPendingOutShares()
{
    SdkMutexGuard guard(sdkMutex);

    sharedNode_vector nodes = client->mNodeManager.getNodesWithPendingOutShares();

    return new MegaShareListPrivate(
        impl::ShareExtractor::extractPendingOutShares(nodes, client->mKeyManager));
}

MegaShareList *MegaApiImpl::getPendingOutShares(MegaNode *megaNode)
{
    if(!megaNode)
    {
        return new MegaShareListPrivate();
    }

    SdkMutexGuard g(sdkMutex);
    std::shared_ptr<Node> node = client->nodebyhandle(megaNode->getHandle());
    if (!node)
    {
        return new MegaShareListPrivate();
    }

    return new MegaShareListPrivate(
        impl::ShareExtractor::extractPendingOutShares({node}, client->mKeyManager));
}

bool MegaApiImpl::isPrivateNode(MegaHandle h)
{
    SdkMutexGuard lock(sdkMutex);

    return client->isPrivateNode(NodeHandle().set6byte(h));
}

bool MegaApiImpl::isForeignNode(MegaHandle h)
{
    SdkMutexGuard lock(sdkMutex);

    return client->isForeignNode(NodeHandle().set6byte(h));
}

MegaNodeList *MegaApiImpl::getPublicLinks(int order)
{
    SdkMutexGuard g(sdkMutex);

    sharedNode_vector vNodes = client->mNodeManager.getNodesWithLinks();

    // avoid to return the link associated to the S4 container, in line with Webclient
    if (client->mIsS4Enabled)
    {
        for (auto it = vNodes.begin(); it != vNodes.end(); ++it)
        {
            if (*it && (*it)->nodeHandle().eq(client->mS4Container))
            {
                vNodes.erase(it);
                break;
            }
        }
    }

    sortByComparatorFunction(vNodes, order, *client);
    return new MegaNodeListPrivate(vNodes);
}

MegaContactRequestList* MegaApiImpl::getIncomingContactRequests() const
{
    SdkMutexGuard g(sdkMutex);
    vector<PendingContactRequest*> vContactRequests;
    for (handlepcr_map::iterator it = client->pcrindex.begin(); it != client->pcrindex.end(); it++)
    {
        if(!it->second->isoutgoing && !it->second->removed())
        {
            vContactRequests.push_back(it->second.get());
        }
    }

    return new MegaContactRequestListPrivate(vContactRequests.data(), int(vContactRequests.size()));
}

MegaContactRequestList* MegaApiImpl::getOutgoingContactRequests() const
{
    SdkMutexGuard g(sdkMutex);
    vector<PendingContactRequest*> vContactRequests;
    for (handlepcr_map::iterator it = client->pcrindex.begin(); it != client->pcrindex.end(); it++)
    {
        if(it->second->isoutgoing && !it->second->removed())
        {
            vContactRequests.push_back(it->second.get());
        }
    }

    return new MegaContactRequestListPrivate(vContactRequests.data(), int(vContactRequests.size()));
}

int MegaApiImpl::getAccess(const std::variant<MegaNode*, MegaHandle>& nodeOrNodeHandle)
{
    MegaHandle nodeHandle{INVALID_HANDLE};

    if (auto node = std::get_if<MegaNode*>(&nodeOrNodeHandle))
    {
        if (*node)
        {
            nodeHandle = (*node)->getHandle();
        }
    }
    else if (auto handle = std::get_if<MegaHandle>(&nodeOrNodeHandle))
    {
        nodeHandle = *handle;
    }

    if (nodeHandle == INVALID_HANDLE)
    {
        return MegaShare::ACCESS_UNKNOWN;
    }

    SdkMutexGuard g(sdkMutex);
    std::shared_ptr<Node> node = client->nodebyhandle(nodeHandle);
    if(!node)
    {
        return MegaShare::ACCESS_UNKNOWN;
    }

    if (!client->loggedin())
    {
        return MegaShare::ACCESS_READ;
    }

    Node* n = node.get();
    Node* root = n;
    accesslevel_t a = OWNER;
    bool inInshare = false;
    while (n)
    {
        if (n->inshare)
        {
            a = n->inshare->access;
            inInshare = true;
            break;
        }
        root = n;
        n = n->parent.get();
    }

    // Process nodes in Vault first.
    // The Vault is read-only, except the Password Manager subtree.
    if (!inInshare && root->type == VAULTNODE)
    {
        const NodeHandle pwmBase = client->getPasswordManagerBase();
        if (pwmBase.isUndef() || !node->hasNHOrHasAncestorWithNH(pwmBase))
        {
            return MegaShare::ACCESS_READ;
        }
    }

    // For a ROOTNODE or RUBBISHNODE the loop above leaves a == OWNER (no parent
    // and no inshare), so the switch returns OWNER here.
    // Then process accesslevel of regular nodes.
    switch(a)
    {
        case RDONLY: return MegaShare::ACCESS_READ;
        case RDWR: return MegaShare::ACCESS_READWRITE;
        case FULL: return MegaShare::ACCESS_FULL;
        default: return MegaShare::ACCESS_OWNER;
    }
}

bool MegaApiImpl::processMegaTree(MegaNode* n, MegaTreeProcessor* processor, bool recursive)
{
    if (!n)
    {
        return true;
    }

    if (!processor)
    {
        return false;
    }

    SdkMutexGuard g(sdkMutex);
    std::shared_ptr<Node> node = NULL;

    if (!n->isForeign() && !n->isPublic())
    {
        node = client->nodebyhandle(n->getHandle());
    }

    if (!node)
    {
        if (n->getType() != FILENODE)
        {
            MegaNodeList *nList = n->getChildren();
            if (nList)
            {
                for (int i = 0; i < nList->size(); i++)
                {
                    MegaNode *child = nList->get(i);
                    if (recursive)
                    {
                        if (!processMegaTree(child, processor))
                        {
                            return 0;
                        }
                    }
                    else
                    {
                        if (!processor->processMegaNode(child))
                        {
                            return 0;
                        }
                    }
                }
            }
        }
        return processor->processMegaNode(n);
    }

    if (node->type != FILENODE)
    {
        sharedNode_list nodeList = client->getChildren(node.get());
        for (sharedNode_list::iterator it = nodeList.begin(); it != nodeList.end(); )
        {
            Node* sharedNode = it->get();
            it++;
            unique_ptr<MegaNode> megaNode(MegaNodePrivate::fromNode(sharedNode));
            if (recursive)
            {
                if (!processMegaTree(megaNode.get(), processor))
                {
                    return 0;
                }
            }
            else
            {
                if (!processor->processMegaNode(megaNode.get()))
                {
                    return 0;
                }
            }
        }
    }
    return processor->processMegaNode(n);
}


MegaNode *MegaApiImpl::createForeignFileNode(MegaHandle handle, const char *key, const char *name, m_off_t size, m_off_t mtime, const char* fingerprintCrc,
                                            MegaHandle parentHandle, const char* privateauth, const char *publicauth, const char *chatauth)
{
    string nodekey;
    string fileattrsting;
    nodekey.resize(strlen(key) * 3 / 4 + 3);
    nodekey.resize(
        static_cast<size_t>(Base64::atob(key, (byte*)nodekey.data(), int(nodekey.size()))));

    string fingerprintStr;
    string sdkFingerprintStr;

    if (fingerprintCrc)
    {
        FileFingerprint ff;
        ff.size = size;
        ff.mtime = mtime;

        int bytesWritten = Base64::atob(fingerprintCrc, (byte*)&ff.crc, sizeof(ff.crc));
        assert(bytesWritten == sizeof(ff.crc));
        if (bytesWritten == sizeof(ff.crc))
        {
            // only contains crc, mtime.  because Node has size serialized separately
            ff.serializefingerprint(&fingerprintStr);

            // prepend size on the front
            sdkFingerprintStr = MegaNodePrivate::addAppPrefixToFingerprint(fingerprintStr, size);
        }
    }

    return new MegaNodePrivate(name, FILENODE, size, mtime, mtime, handle, &nodekey, &fileattrsting,
                               sdkFingerprintStr.empty() ? nullptr : sdkFingerprintStr.c_str(), NULL, INVALID_HANDLE,
                               parentHandle, privateauth, publicauth, false, true, chatauth, true);
}

MegaNode *MegaApiImpl::createForeignFolderNode(MegaHandle handle, const char *name, MegaHandle parentHandle, const char *privateauth, const char *publicauth)
{
    string nodekey;
    string fileattrsting;
    return new MegaNodePrivate(name, FOLDERNODE, 0, 0, 0, handle, &nodekey, &fileattrsting, NULL, NULL, INVALID_HANDLE, parentHandle,
                               privateauth, publicauth, false, true);
}

MegaNode *MegaApiImpl::authorizeNode(MegaNode *node)
{
    if (!node)
    {
        return NULL;
    }

    if (node->isPublic() || node->isForeign())
    {
        return node->copy();
    }

    MegaNodePrivate *result = NULL;
    SdkMutexGuard g(sdkMutex);
    std::shared_ptr<Node> n = client->nodebyhandle(node->getHandle());
    if (n)
    {
        result = new MegaNodePrivate(node);
        authorizeMegaNodePrivate(result);
    }
    return result;
}

void MegaApiImpl::authorizeMegaNodePrivate(MegaNodePrivate *node)
{
    // Versions are not added to authorized MegaNodes.
    // If it's decided to do so, it's needed to modify the processing
    // of MegaNode copies accordingly

    node->setForeign(true);
    if (node->getType() == MegaNode::TYPE_FILE)
    {
        char *h = NULL;
        if (client->sid.size())
        {
            h = getAccountAuth();
            node->setPrivateAuth(h);
        }
        else
        {
            h = MegaApiImpl::handleToBase64(client->mNodeManager.getRootNodeFiles().as8byte());
            node->setPublicAuth(h);
        }
        delete [] h;
    }
    else
    {
        MegaNodeList *children = getChildren(node, MegaApi::ORDER_NONE);
        node->setChildren(children);
        for (int i = 0; i < children->size(); i++)
        {
            MegaNodePrivate *privNode = (MegaNodePrivate *)children->get(i);
            authorizeMegaNodePrivate(privNode);
        }
    }
}

MegaNode *MegaApiImpl::authorizeChatNode(MegaNode *node, const char *cauth)
{
    if (!node)
    {
        return NULL;
    }

    MegaNodePrivate *result = new MegaNodePrivate(node);
    result->setChatAuth(cauth);

    return result;
}

const char *MegaApiImpl::getVersion()
{
    return client->version();
}

char *MegaApiImpl::getOperatingSystemVersion()
{
    string version;
    fsAccess->osversion(&version, false);
    return MegaApi::strdup(version.c_str());
}

void MegaApiImpl::setPSA(int id, MegaRequestListener *listener)
{
    std::ostringstream oss;
    oss << id;
    string value = oss.str();
    setUserAttr(MegaApi::USER_ATTR_LAST_PSA, value.c_str(), listener);
}

void MegaApiImpl::disableGfxFeatures(bool disable)
{
    client->gfxdisabled = disable;
}

bool MegaApiImpl::areGfxFeaturesDisabled()
{
    return !client->gfx || client->gfxdisabled;
}

const char *MegaApiImpl::getUserAgent()
{
    return client->useragent.c_str();
}

const char *MegaApiImpl::getBasePath()
{
    return basePath.c_str();
}

void MegaApiImpl::changeApiUrl(const char *apiURL, bool disablepkp)
{
    {
        // change defaults for future MegaApi construction
        lock_guard<mutex> g(g_APIURL_default_mutex);
        g_APIURL_default = apiURL;
        g_disablepkp_default = disablepkp;
    }

    // change this MegaApi too
    SdkMutexGuard g(sdkMutex);
    client->httpio->APIURL = apiURL;
    client->httpio->disablepkp = disablepkp;

    client->abortbackoff();
    client->disconnect();
}

bool MegaApiImpl::setLanguage(const char *languageCode)
{
    string code;
    if (!getLanguageCode(languageCode, &code))
    {
        return false;
    }

    SdkMutexGuard g(sdkMutex);
    return client->setlang(&code);
}

int MegaApiImpl::enableSearchDBIndexes(bool enable)
{
    if (client->loggedin() != sessiontype_t::NOTLOGGEDIN)
    {
        LOG_warn << "This method should be called before login";
        return API_EACCESS;
    }

    client->enableSearchDBIndexes(enable);
    return API_OK;
}

int MegaApiImpl::enableLexicographicDBIndexes(bool enable)
{
    if (client->loggedin() != sessiontype_t::NOTLOGGEDIN)
    {
        LOG_warn << "This method should be called before login";
        return API_EACCESS;
    }

    client->enableLexicographicDBIndexes(enable);
    return API_OK;
}

string MegaApiImpl::generateViewId()
{
    return MegaClient::generateViewId(client->rng);
}

void MegaApiImpl::setLanguagePreference(const char *languageCode, MegaRequestListener *listener)
{
    setUserAttr(MegaApi::USER_ATTR_LANGUAGE, languageCode, listener);
}

void MegaApiImpl::getLanguagePreference(MegaRequestListener *listener)
{
    getUserAttr(NULL, MegaApi::USER_ATTR_LANGUAGE, NULL, 0, listener);
}

bool MegaApiImpl::getLanguageCode(const char *languageCode, string *code)
{
    if (!languageCode || !code)
    {
        return false;
    }

    size_t len = strlen(languageCode);
    if (len < 2 || len > 7)
    {
        return false;
    }

    code->clear();
    string s = languageCode;
    tolower_string(s);

    while (s.size() >= 2)
    {
        JSON json;
        nameid id = json.getnameid(s.c_str());
        switch (id)
        {
            // Regular language codes
            case makeNameid("ar"):
            case makeNameid("bg"):
            case makeNameid("de"):
            case makeNameid("en"):
            case makeNameid("es"):
            case makeNameid("fa"):
            case makeNameid("fi"):
            case makeNameid("fr"):
            case makeNameid("he"):
            case makeNameid("hu"):
            case makeNameid("id"):
            case makeNameid("it"):
            case makeNameid("nl"):
            case makeNameid("pl"):
            case makeNameid("ro"):
            case makeNameid("ru"):
            case makeNameid("sk"):
            case makeNameid("sl"):
            case makeNameid("sr"):
            case makeNameid("th"):
            case makeNameid("tl"):
            case makeNameid("tr"):
            case makeNameid("uk"):
            case makeNameid("vi"):

            // Not used on apps
            case makeNameid("cz"):
            case makeNameid("jp"):
            case makeNameid("kr"):
            case makeNameid("br"):
            case makeNameid("se"):
            case makeNameid("cn"):
            case makeNameid("ct"):
                *code = s;
                break;

            // Conversions
            case makeNameid("cs"):
                *code = "cz";
                break;

            case makeNameid("ja"):
                *code = "jp";
                break;

            case makeNameid("ko"):
                *code = "kr";
                break;

            case makeNameid("pt"):
            case makeNameid("pt_br"):
            case makeNameid("pt-br"):
            case makeNameid("pt_pt"):
            case makeNameid("pt-pt"):
                *code = "br";
                break;

            case makeNameid("sv"):
                *code = "se";
                break;

            case makeNameid("zh"):
            case makeNameid("zh_cn"):
            case makeNameid("zh-cn"):
            case makeNameid("zh_hans"):
            case makeNameid("zh-hans"):
                *code = "cn";
                break;

            case makeNameid("zh_tw"):
            case makeNameid("zh-tw"):
            case makeNameid("zh_hant"):
            case makeNameid("zh-hant"):
                *code = "ct";
                break;

            case makeNameid("in"):
                *code = "id";
                break;

            case makeNameid("iw"):
                *code = "he";
                break;

            // Not supported in the web
            case makeNameid("ee"):
            case makeNameid("hr"):
            case makeNameid("ka"):
                break;

            default:
                LOG_debug << "Unknown language code: " << s.c_str();
                break;
        }

        if (code->size())
        {
            return true;
        }

        s.resize(s.size() - 1);
    }

    LOG_debug << "Unsupported language code: " << languageCode;
    return false;
}

void MegaApiImpl::setFileVersionsOption(bool disable, MegaRequestListener *listener)
{
    string av = disable ? "1" : "0";
    setUserAttr(MegaApi::USER_ATTR_DISABLE_VERSIONS, av.data(), listener);
}

void MegaApiImpl::getFileVersionsOption(MegaRequestListener *listener)
{
    getUserAttr(NULL, MegaApi::USER_ATTR_DISABLE_VERSIONS, NULL, 0, listener);
}

void MegaApiImpl::setContactLinksOption(bool enable, MegaRequestListener* listener)
{
    string av = enable ? "1" : "0";
    setUserAttr(MegaApi::USER_ATTR_CONTACT_LINK_VERIFICATION, av.data(), listener);
}

void MegaApiImpl::getContactLinksOption(MegaRequestListener *listener)
{
    getUserAttr(NULL, MegaApi::USER_ATTR_CONTACT_LINK_VERIFICATION, NULL, 0, listener);
}

void MegaApiImpl::retrySSLerrors(bool enable)
{
    SdkMutexGuard g(sdkMutex);
    client->retryessl = enable;
}

void MegaApiImpl::setPublicKeyPinning(bool enable)
{
    SdkMutexGuard g(sdkMutex);
    client->httpio->disablepkp = !enable;
}

// No SdkMutexGuard on this setter/getter pair: the underlying storage is
// std::atomic<bool>, and the flag is independent of any client-side state
// protected by sdkMutex.
void MegaApiImpl::setUsePlatformAvailableDiskSpaceQuery(bool enable)
{
    FileSystemAccess::setUsePlatformAvailableDiskSpaceQuery(enable);
}

bool MegaApiImpl::usePlatformAvailableDiskSpaceQuery() const
{
    return FileSystemAccess::usePlatformAvailableDiskSpaceQuery();
}

void MegaApiImpl::pauseActionPackets()
{
    SdkMutexGuard g(sdkMutex);
    LOG_debug << "Pausing action packets";
    client->scpaused = true;
}

void MegaApiImpl::resumeActionPackets()
{
    SdkMutexGuard g(sdkMutex);
    LOG_debug << "Resuming action packets";
    client->scpaused = false;
}

bool MegaApiImpl::isValidTypeNode(const Node *node, int type) const
{
    assert(node);
    if (!client)
    {
        return true;
    }

    switch (type)
    {
        case MegaApi::FILE_TYPE_PHOTO:
            return client->nodeIsPhoto(node, false);
        case MegaApi::FILE_TYPE_AUDIO:
            return client->nodeIsAudio(node);
        case MegaApi::FILE_TYPE_VIDEO:
            return client->nodeIsVideo(node);
        case MegaApi::FILE_TYPE_DOCUMENT:
            return client->nodeIsDocument(node);
        case MegaApi::FILE_TYPE_PDF:
            return client->nodeIsPdf(node);
        case MegaApi::FILE_TYPE_PRESENTATION:
            return client->nodeIsPresentation(node);
        case MegaApi::FILE_TYPE_ARCHIVE:
            return client->nodeIsArchive(node);
        case MegaApi::FILE_TYPE_PROGRAM:
            return client->nodeIsProgram(node);
        case MegaApi::FILE_TYPE_MISC:
            return client->nodeIsMiscellaneous(node);
        case MegaApi::FILE_TYPE_SPREADSHEET:
            return client->nodeIsSpreadsheet(node);
        case MegaApi::FILE_TYPE_ALL_DOCS:
            return client->nodeIsDocument(node) || client->nodeIsPdf(node) ||
                   client->nodeIsPresentation(node) || client->nodeIsSpreadsheet(node);
        case MegaApi::FILE_TYPE_ALL_VISUAL_MEDIA:
            return client->nodeIsPhoto(node, false) || client->nodeIsVideo(node);
        case MegaApi::FILE_TYPE_OTHERS:
            return client->nodeIsOtherType(node);
        case MegaApi::FILE_TYPE_DEFAULT:
        default:
            return true;
    }
}

// map to an ACCOUNT_* value to an int that can be compared:
// free -> starter -> basic -> essential -> lite -> proi -> proii -> proiii
// the ACCOUNT_* values are out of order
// int proLevel is a MegaAccountDetails::ACCOUNT_*
static inline int orderProLevel(int proLevel)
{
    switch (proLevel)
    {
        case MegaAccountDetails::ACCOUNT_TYPE_STARTER:
            return 1;
        case MegaAccountDetails::ACCOUNT_TYPE_BASIC:
            return 2;
        case MegaAccountDetails::ACCOUNT_TYPE_ESSENTIAL:
            return 3;
        case MegaAccountDetails::ACCOUNT_TYPE_LITE:
            return 4;
        case MegaAccountDetails::ACCOUNT_TYPE_PROI:
            return 5;
        case MegaAccountDetails::ACCOUNT_TYPE_PROII:
            return 6;
        case MegaAccountDetails::ACCOUNT_TYPE_PROIII:
            return 7;
        default:
            return 0;
    }
}

int MegaApiImpl::calcRecommendedProLevel(MegaPricing& pricing, MegaAccountDetails& details)
{
    // if this algorithm changes also have the webclient implementation updated
    int currProLevel = details.getProLevel();
    if (currProLevel == MegaAccountDetails::ACCOUNT_TYPE_BUSINESS || currProLevel == MegaAccountDetails::ACCOUNT_TYPE_PRO_FLEXI)
        return currProLevel;
        // business can not upgrade, flexi can only change to free so we do not recommend that
    int orderedCurrProLevel = orderProLevel(currProLevel);
    uint64_t usedStorageBytes = static_cast<uint64_t>(details.getStorageUsed());
    int bestProLevel = -1;
    uint64_t bestStorageBytes = UINT64_MAX;
    for (int i = 0; i <= pricing.getNumProducts(); ++i)
    {
        // only upgrade to starter, basic, essential, lite, pro1, pro2 and pro3
        int planProLevel = pricing.getProLevel(i);
        if (planProLevel < MegaAccountDetails::ACCOUNT_TYPE_PROI || planProLevel > MegaAccountDetails::ACCOUNT_TYPE_ESSENTIAL)
            continue;
        // only monthly plans
        int planMonths = pricing.getMonths(i);
        if (planMonths != 1)
            continue;
        // must have enough space for user's data
        int planStorageGb = pricing.getGBStorage(i);
        if (planStorageGb < 0)
        {
            assert(false && "business plan, should never happen");
            continue;
        }
        uint64_t planStorageBytes = (uint64_t)planStorageGb * (uint64_t)(1024 * 1024 * 1024);
        if (usedStorageBytes > planStorageBytes)
            continue;
        // must be an upgrade free->starter->basic->essential->lite->proi->proii->proiii
        int orderedPlanProLevel = orderProLevel(planProLevel);
        if (orderedCurrProLevel >= orderedPlanProLevel)
            continue;
        // get smallest storage
        if (planStorageBytes >= bestStorageBytes)
            continue;
        bestProLevel = planProLevel;
        bestStorageBytes = planStorageBytes;
    }
    if (bestStorageBytes != UINT64_MAX)
    {
        assert(bestProLevel != -1);
        return bestProLevel;
    }
    // too much storage required
    return MegaAccountDetails::ACCOUNT_TYPE_PRO_FLEXI;
}

MegaNodeList* MegaApiImpl::search(const MegaSearchFilter* filter, int order, CancelToken cancelToken, const MegaSearchPage* searchPage)
{
    // guard against unsupported or removed order criteria
    assert((MegaApi::ORDER_NONE <= order && order <= MegaApi::ORDER_MODIFICATION_DESC) ||
           (MegaApi::ORDER_LABEL_ASC <= order && order <= MegaApi::ORDER_FAV_DESC));

    if (!filter ||
        (filter->byNodeType() == MegaNode::TYPE_FOLDER && filter->byCategory() != MegaApi::FILE_TYPE_DEFAULT))
    {
        return new MegaNodeListPrivate();
    }

    sharedNode_vector searchResults;

    // search
    {
        SdkMutexGuard g(sdkMutex);

        switch (filter->byLocation())
        {
        case MegaApi::SEARCH_TARGET_ALL:
        case MegaApi::SEARCH_TARGET_ROOTNODE: // Search on Cloud root and Vault, excluding Rubbish
        case MegaApi::SEARCH_TARGET_INSHARE:
        case MegaApi::SEARCH_TARGET_OUTSHARE:
        case MegaApi::SEARCH_TARGET_PUBLICLINK:
            searchResults = searchInNodeManager(filter, order, cancelToken, searchPage);
            break;
        default:
            LOG_err << "Search not implemented for Location " << filter->byLocation();
        }
    } // end scope for mutex guard

    MegaNodeListPrivate* nodeList = new MegaNodeListPrivate(searchResults);

    return nodeList;
}

namespace
{
/**
 * @brief A helper function to convert a MegaSearchFilter (external layer) into a NodeSearchFilter
 * (internal).
 *
 * @param filter The filter to convert
 * @param includedShares (Optional) What kind of share nodes to check in the search
 * @return A NodeSearchFilter object to be used in search methods from the NodeManager
 */
NodeSearchFilter searchToNodeFilter(const MegaSearchFilter& filter,
                                    const ShareType_t includedShares = NO_SHARES)
{
    NodeSearchFilter nf;
    nf.byName(filter.byName());
    nf.byNodeType(static_cast<nodetype_t>(filter.byNodeType()));
    nf.byCategory(static_cast<MimeType_t>(filter.byCategory()));
    nf.bySensitivity(static_cast<NodeSearchFilter::BoolFilter>(filter.bySensitivity()));
    nf.byFavourite(static_cast<NodeSearchFilter::BoolFilter>(filter.byFavourite()));
    nf.byLocationHandle(filter.byLocationHandle());
    nf.setIncludedShares(includedShares);
    nf.byCreationTimeLowerLimitInSecs(filter.byCreationTimeLowerLimit());
    nf.byCreationTimeUpperLimitInSecs(filter.byCreationTimeUpperLimit());
    nf.byModificationTimeLowerLimitInSecs(filter.byModificationTimeLowerLimit());
    nf.byModificationTimeUpperLimitInSecs(filter.byModificationTimeUpperLimit());
    nf.byDescription(filter.byDescription());
    nf.byTag(filter.byTag());
    nf.useAndForTextQuery(filter.useAndForTextQuery());
    return nf;
}
}

sharedNode_vector MegaApiImpl::searchInNodeManager(const MegaSearchFilter* filter, int order, CancelToken cancelToken, const MegaSearchPage* searchPage)
{
    int shareType =
        filter->byLocation() == MegaApi::SEARCH_TARGET_INSHARE ?
            IN_SHARES :
            (filter->byLocation() == MegaApi::SEARCH_TARGET_OUTSHARE ?
                 static_cast<ShareType_t>(OUT_SHARES | PENDING_OUTSHARES) :
                 (filter->byLocation() == MegaApi::SEARCH_TARGET_PUBLICLINK ? LINK : NO_SHARES));

    NodeSearchFilter nf = searchToNodeFilter(*filter, static_cast<ShareType_t>(shareType));

    if (filter->byLocation() == MegaApi::SEARCH_TARGET_ROOTNODE)
    {
        // search under Cloud root and Vault
        nf.byAncestors({ client->mNodeManager.getRootNodeFiles().as8byte(),
                         client->mNodeManager.getRootNodeVault().as8byte(),
                         UNDEF });
    }
    else if (filter->byLocation() == MegaApi::SEARCH_TARGET_ALL && filter->byLocationHandle() == INVALID_HANDLE)
    {
        // search under Cloud root, Vault, Rubbish and among in-shares
        nf.byAncestors({ client->mNodeManager.getRootNodeFiles().as8byte(),
                         client->mNodeManager.getRootNodeVault().as8byte(),
                         client->mNodeManager.getRootNodeRubbish().as8byte() });
        nf.setIncludedShares(IN_SHARES);
    }

    const NodeSearchPage& np = searchPage ? NodeSearchPage(searchPage->startingOffset(), searchPage->size()) : NodeSearchPage(0, 0);
    sharedNode_vector results = client->mNodeManager.searchNodes(nf, order, cancelToken, np);
    return results;
}

// nodemanager.h cannot include megaapi.h (layering), so the default scope and
// the location-handles cap are duplicated as kDefaultListAllLocationScope and
// kListAllMaxLocationHandles. This translation unit sees both the duplicated
// constants and the public-API constants, and is the natural place to enforce
// that they stay in sync.
static_assert(kDefaultListAllLocationScope == MegaNodeScopeFilter::LOCATION_CLOUD_DRIVE_AND_VAULT,
              "kDefaultListAllLocationScope must mirror "
              "MegaNodeScopeFilter::LOCATION_CLOUD_DRIVE_AND_VAULT");
static_assert(kListAllMaxLocationHandles == MegaNodeScopeFilter::MAX_LOCATION_HANDLES,
              "kListAllMaxLocationHandles must mirror "
              "MegaNodeScopeFilter::MAX_LOCATION_HANDLES");

namespace
{
// Validates a MegaHandleList (size cap + INVALID_HANDLE rejection) and converts
// it to NodeHandle, appending to `out`. Null list is treated as empty.
bool extractListAllHandles(const MegaHandleList* listOwner,
                           const char* fieldName,
                           std::vector<NodeHandle>& out,
                           const char* logPrefix)
{
    if (!listOwner)
        return true;
    const unsigned n = listOwner->size();
    if (n > MegaNodeScopeFilter::MAX_LOCATION_HANDLES)
    {
        LOG_warn << logPrefix << ": " << fieldName << " size " << n
                 << " exceeds MAX_LOCATION_HANDLES=" << MegaNodeScopeFilter::MAX_LOCATION_HANDLES;
        return false;
    }
    for (unsigned i = 0; i < n; ++i)
    {
        const MegaHandle h = listOwner->get(i);
        if (h == INVALID_HANDLE)
        {
            LOG_warn << logPrefix << ": " << fieldName
                     << " contains INVALID_HANDLE entry at position " << i;
            return false;
        }
        NodeHandle nh;
        nh.set6byte(h);
        out.push_back(nh);
    }
    return true;
}

// Builds a NodeSearchCursorOffset from megaCursor, validating the fields the
// chosen sort order requires. Returns std::nullopt on validation failure.
// The caller must have already accepted `order` as a supported value.
std::optional<NodeSearchCursorOffset>
    buildNodeSearchCursorOffset(const MegaSearchCursorOffset& megaCursor, int order)
{
    NodeSearchCursorOffset c;

    const char* lastName = megaCursor.getLastName();
    if (!lastName || !*lastName)
    {
        LOG_warn << "listAllNodesByPage: cursor has missing or empty last name.";
        return std::nullopt;
    }
    const MegaHandle lastHandle = megaCursor.getLastHandle();
    if (lastHandle == INVALID_HANDLE)
    {
        LOG_warn << "listAllNodesByPage: cursor has invalid last handle.";
        return std::nullopt;
    }
    c.mLastName = lastName;
    c.mLastHandle = static_cast<handle>(lastHandle);

    switch (order)
    {
        case MegaApi::ORDER_DEFAULT_ASC:
        case MegaApi::ORDER_DEFAULT_DESC:
            break;
        case MegaApi::ORDER_SIZE_ASC:
        case MegaApi::ORDER_SIZE_DESC:
        {
            const int64_t lastSize = megaCursor.getLastSize();
            if (lastSize < 0)
            {
                LOG_warn << "listAllNodesByPage: cursor has missing or negative last size: "
                         << lastSize;
                return std::nullopt;
            }
            c.mLastSize = lastSize;
            break;
        }
        case MegaApi::ORDER_MODIFICATION_ASC:
        case MegaApi::ORDER_MODIFICATION_DESC:
        {
            const int64_t lastMtime = megaCursor.getLastMtime();
            if (lastMtime < 0)
            {
                LOG_warn << "listAllNodesByPage: cursor has missing or invalid last mtime: "
                         << lastMtime;
                return std::nullopt;
            }
            c.mLastMtime = lastMtime;
            break;
        }
        case MegaApi::ORDER_LABEL_ASC:
        case MegaApi::ORDER_LABEL_DESC:
        {
            const int lastLabel = megaCursor.getLastLabel();
            if (lastLabel < MegaNode::NODE_LBL_UNKNOWN || lastLabel > MegaNode::NODE_LBL_GREY)
            {
                LOG_warn << "listAllNodesByPage: cursor has missing or out-of-range last label: "
                         << lastLabel;
                return std::nullopt;
            }
            c.mLastLabel = lastLabel;
            break;
        }
        case MegaApi::ORDER_FAV_ASC:
        case MegaApi::ORDER_FAV_DESC:
        {
            const int lastFav = megaCursor.getLastFav();
            if (lastFav != 0 && lastFav != 1)
            {
                LOG_warn << "listAllNodesByPage: cursor has missing or invalid last fav value: "
                         << lastFav;
                return std::nullopt;
            }
            c.mLastFav = lastFav;
            break;
        }
        default:
            // Caller is responsible for filtering out unsupported orders.
            assert(false && "buildNodeSearchCursorOffset: unsupported order leaked through");
            LOG_warn << "listAllNodesByPage: unsupported order value: " << order;
            return std::nullopt;
    }

    return c;
}
} // anonymous namespace

bool MegaApiImpl::parseListAllFilterIntoBase(const MegaNodeScopeFilter* filter,
                                             const char* logPrefix,
                                             ListAllFilterParams& out) const
{
    if (!filter)
    {
        LOG_warn << logPrefix << ": filter is nullptr";
        return false;
    }

    const int mimeType = filter->byCategory();
    if (mimeType <= MegaApi::FILE_TYPE_DEFAULT || mimeType > MegaApi::FILE_TYPE_LAST)
    {
        LOG_warn << logPrefix << ": invalid byCategory value " << mimeType;
        return false;
    }

    const int locationScope = filter->byLocation();
    switch (locationScope)
    {
        case MegaNodeScopeFilter::LOCATION_CLOUD_DRIVE:
        case MegaNodeScopeFilter::LOCATION_CLOUD_DRIVE_AND_VAULT:
        case MegaNodeScopeFilter::LOCATION_CLOUD_DRIVE_VAULT_AND_RUBBISH:
            break;
        default:
            LOG_warn << logPrefix << ": invalid byLocation value " << locationScope;
            return false;
    }

    std::vector<NodeHandle> explicitAncestors;
    std::vector<NodeHandle> excludeHandles;
    std::unique_ptr<MegaHandleList> includeList(filter->byLocationHandles());
    if (!extractListAllHandles(includeList.get(),
                               "byLocationHandles",
                               explicitAncestors,
                               logPrefix))
        return false;
    std::unique_ptr<MegaHandleList> excludeList(filter->byExcludeLocationHandles());
    if (!extractListAllHandles(excludeList.get(),
                               "byExcludeLocationHandles",
                               excludeHandles,
                               logPrefix))
        return false;

    out.mimeType = static_cast<MimeType_t>(mimeType);
    out.excludeSensitive =
        (filter->bySensitivity() == MegaNodeScopeFilter::SENSITIVITY_HIDE_SENSITIVE);
    out.explicitAncestors = std::move(explicitAncestors);
    out.excludeHandles = std::move(excludeHandles);
    out.locationScope = locationScope;
    return true;
}

std::optional<ListAllNodesParams>
    MegaApiImpl::buildListAllParams(const MegaListAllNodesFilter* filter,
                                    int order,
                                    size_t maxElements,
                                    const MegaSearchCursorOffset* megaCursor) const
{
    ListAllNodesParams params;
    if (!parseListAllFilterIntoBase(filter, "listAllNodesByPage", params))
        return std::nullopt;

    // ── Order validation (independent of cursor presence) ───────────────────
    switch (order)
    {
        case MegaApi::ORDER_DEFAULT_ASC:
        case MegaApi::ORDER_DEFAULT_DESC:
        case MegaApi::ORDER_SIZE_ASC:
        case MegaApi::ORDER_SIZE_DESC:
        case MegaApi::ORDER_MODIFICATION_ASC:
        case MegaApi::ORDER_MODIFICATION_DESC:
        case MegaApi::ORDER_LABEL_ASC:
        case MegaApi::ORDER_LABEL_DESC:
        case MegaApi::ORDER_FAV_ASC:
        case MegaApi::ORDER_FAV_DESC:
            break;
        default:
            LOG_warn << "listAllNodesByPage: unsupported order value: " << order;
            return std::nullopt;
    }
    params.order = order;
    params.maxElements = maxElements;

    // ── Cursor validation (only when supplied) ──────────────────────────────
    if (megaCursor)
    {
        auto cursor = buildNodeSearchCursorOffset(*megaCursor, order);
        if (!cursor)
            return std::nullopt;
        params.cursor = std::move(cursor);
    }

    {
        const int64_t anchorStart = filter->byTimestampAnchorStartDate();
        const int64_t anchorEnd = filter->byTimestampAnchorEndDate();
        const int anchorOrder = filter->byTimestampAnchorOrder();
        const bool anchorSet = (anchorStart != 0 || anchorEnd != 0 || anchorOrder != -1);

        if (anchorSet)
        {
            if (!isSupportedTimestampOrder(anchorOrder))
            {
                LOG_warn << "listAllNodesByPage: unsupported byTimestampAnchor sectionOrder "
                         << anchorOrder;
                return std::nullopt;
            }

            if (anchorStart < 0 || anchorEnd < 0)
            {
                LOG_warn << "listAllNodesByPage: byTimestampAnchor has a negative bound";
                return std::nullopt;
            }

            if (anchorStart >= anchorEnd)
            {
                LOG_warn << "listAllNodesByPage: byTimestampAnchor startDate >= endDate";
                return std::nullopt;
            }

            TimestampAnchorFilter ta;
            ta.mOrder = anchorOrder;
            ta.mStartSeconds = anchorStart;
            ta.mEndSeconds = anchorEnd;
            params.timestampAnchor = ta;
        }
    }

    return params;
}

std::optional<int64_t> parseUtcOffsetSeconds(const char* tz)
{
    if (!tz || tz[0] == '\0')
        return 0; // unset → UTC

    const std::string s{tz};
    // Exact shape: sign HH ':' MM  → length 6, digits at 1,2,4,5, ':' at 3.
    if (s.size() != 6 || (s[0] != '+' && s[0] != '-') || s[3] != ':')
        return std::nullopt;
    for (const size_t i: {1u, 2u, 4u, 5u})
        if (s[i] < '0' || s[i] > '9')
            return std::nullopt;

    const int hours = (s[1] - '0') * 10 + (s[2] - '0');
    const int minutes = (s[4] - '0') * 10 + (s[5] - '0');
    if (minutes > 59)
        return std::nullopt;

    int64_t total = static_cast<int64_t>(hours) * 3600 + static_cast<int64_t>(minutes) * 60;
    if (s[0] == '-')
        total = -total;

    // Real-world offsets only: UTC-12:00 .. UTC+14:00.
    if (total < -12 * 3600 || total > 14 * 3600)
        return std::nullopt;

    return total;
}

std::optional<DateSectionParams>
    MegaApiImpl::buildDateSectionParams(const MegaGroupNodesByDateFilter* filter, int order) const
{
    // Sole order gate on this path — keeps the builder safe for any future
    // internal caller, not just the API entry.
    if (!isSupportedTimestampOrder(order))
    {
        LOG_warn << "groupAllNodesByDate: unsupported order " << order;
        return std::nullopt;
    }

    DateSectionParams params;
    // parseListAllFilterIntoBase null-checks the filter, so byGranularity() below
    // is only read once it's known non-null.
    if (!parseListAllFilterIntoBase(filter, "groupAllNodesByDate", params))
        return std::nullopt;

    const int granularity = filter->byGranularity();
    if (granularity < MegaGroupNodesByDateFilter::SECTION_GRANULARITY_DAY ||
        granularity > MegaGroupNodesByDateFilter::SECTION_GRANULARITY_YEAR)
    {
        LOG_warn << "groupAllNodesByDate: invalid granularity " << granularity;
        return std::nullopt;
    }

    const auto tzOffset = parseUtcOffsetSeconds(filter->byUtcOffset());
    if (!tzOffset)
    {
        LOG_warn << "groupAllNodesByDate: invalid UTC offset \"" << filter->byUtcOffset() << "\"";
        return std::nullopt;
    }

    params.order = order;
    params.granularity = static_cast<DateSectionGranularity>(granularity);
    params.tzOffsetSeconds = *tzOffset;
    return params;
}

MegaNodeList* MegaApiImpl::runListAllNodesByPage(const std::optional<ListAllNodesParams>& params,
                                                 CancelToken cancelToken)
{
    if (!params)
        return new MegaNodeListPrivate();

    SdkMutexGuard g(sdkMutex);
    sharedNode_vector results = client->mNodeManager.listAllNodesByPage(*params, cancelToken);
    return new MegaNodeListPrivate(results);
}

MegaNodeList* MegaApiImpl::listAllNodesByPage(const MegaListAllNodesFilter* filter,
                                              int order,
                                              CancelToken cancelToken,
                                              size_t maxElements,
                                              const MegaSearchCursorOffset* megaCursor)
{
    return runListAllNodesByPage(buildListAllParams(filter, order, maxElements, megaCursor),
                                 cancelToken);
}

MegaNodeList* MegaApiImpl::listAllNodesByPageAtOffset(const MegaListAllNodesFilter* filter,
                                                      int order,
                                                      CancelToken cancelToken,
                                                      size_t maxElements,
                                                      int64_t offset)
{
    if (offset < 0)
    {
        LOG_warn << "listAllNodesByPageAtOffset: negative offset (" << offset << ")";
        return new MegaNodeListPrivate();
    }

    auto params = buildListAllParams(filter, order, maxElements, /*cursor=*/nullptr);
    if (params)
        params->offset = offset;
    return runListAllNodesByPage(params, cancelToken);
}

MegaDateSectionList* MegaApiImpl::groupAllNodesByDate(const MegaGroupNodesByDateFilter* filter,
                                                      int order,
                                                      CancelToken cancelToken)
{
    auto paramsOpt = buildDateSectionParams(filter, order);
    if (!paramsOpt)
        return new MegaDateSectionListPrivate();

    SdkMutexGuard g(sdkMutex);
    std::vector<DateSection> sections =
        client->mNodeManager.groupAllNodesByDate(*paramsOpt, cancelToken);

    std::vector<MegaDateSectionPrivate> result;
    result.reserve(sections.size());
    for (auto& s: sections)
        result.emplace_back(std::move(s.mGroupId), s.mStartDate, s.mEndDate, s.mCount);
    return new MegaDateSectionListPrivate(std::move(result));
}

long long MegaApiImpl::getSize(MegaNode *n)
{
    if(!n) return 0;

    if (n->getType() == MegaNode::TYPE_FILE)
    {
       return n->getSize();
    }

    if (n->isForeign())
    {
        MegaSizeProcessor megaSizeProcessor;
        processMegaTree(n, &megaSizeProcessor);
        return megaSizeProcessor.getTotalBytes();
    }

    SdkMutexGuard g(sdkMutex);
    std::shared_ptr<Node> node = client->nodebyhandle(n->getHandle());
    if(!node)
    {
        return 0;
    }

    NodeCounter nodeCounter = node->getCounter();
    return nodeCounter.storage;
}

char *MegaApiImpl::getFingerprint(const char *filePath)
{
    if(!filePath) return NULL;

    auto localpath = LocalPath::fromAbsolutePath(filePath);

    auto fa = fsAccess->newfileaccess();
    if (!fa->fopen(localpath, OPEN_RDONLY, FSLogging::logOnError))
        return NULL;

    FileFingerprint fp;
    fp.genfingerprint(fa.get());
    m_off_t size = fa->size;
    if(fp.size < 0)
        return NULL;

    string fingerprint;
    fp.serializefingerprint(&fingerprint);
    string result = MegaNodePrivate::addAppPrefixToFingerprint(fingerprint, size);

    return MegaApi::strdup(result.c_str());
}

void MegaApiImpl::transfer_failed(Transfer* t, const Error& e, dstime timeleft)
{
    for (file_list::iterator it = t->files.begin(); it != t->files.end(); it++)
    {
        MegaTransferPrivate* transfer = getMegaTransferPrivate((*it)->tag);
        if (!transfer)
        {
            continue;
        }
        processTransferFailed(t, transfer, e, timeleft);
    }
}

char *MegaApiImpl::getFingerprint(MegaInputStream *inputStream, int64_t mtime)
{
    if(!inputStream) return NULL;

    ExternalInputStream is(inputStream);
    m_off_t size = is.size();
    if(size < 0)
        return NULL;

    FileFingerprint fp;
    fp.genfingerprint(&is, mtime);

    if(fp.size < 0)
        return NULL;

    string fingerprint;
    fp.serializefingerprint(&fingerprint);
    string result = MegaNodePrivate::addAppPrefixToFingerprint(fingerprint, size);

    return MegaApi::strdup(result.c_str());
}

MegaNode *MegaApiImpl::getNodeByFingerprint(const char *fingerprint)
{
    if(!fingerprint) return NULL;

    SdkMutexGuard g(sdkMutex);
    return MegaNodePrivate::fromNode(getNodeByFingerprintInternal(fingerprint).get());
}

MegaNodeList* MegaApiImpl::getNodesByFingerprint(const char* fingerprint, const bool excludeMtime)
{
    unique_ptr<FileFingerprint> fp(MegaApiImpl::getFileFingerprintInternal(fingerprint));
    if (!fp)
    {
        return new MegaNodeListPrivate();
    }

    SdkMutexGuard g(sdkMutex);
    sharedNode_vector nodes = client->mNodeManager.getNodesByFingerprint(*fp, excludeMtime);
    return new MegaNodeListPrivate(nodes);
}

MegaNodeList *MegaApiImpl::getNodesByOriginalFingerprint(const char *originalfingerprint, MegaNode* megaparent)
{
    SdkMutexGuard g(sdkMutex);
    std::shared_ptr<Node> parent = megaparent ? client->nodebyhandle(megaparent->getHandle()) : NULL;

    if (!originalfingerprint || (megaparent && (!parent || parent->type == FILENODE)))
    {
        return new MegaNodeListPrivate();
    }

    sharedNode_vector nodes = client->mNodeManager.getNodesByOrigFingerprint(originalfingerprint, parent.get());
    MegaNodeList *result = new MegaNodeListPrivate(nodes);
    return result;
}

MegaNode *MegaApiImpl::getExportableNodeByFingerprint(const char *fingerprint, const char *name)
{
    MegaNode *result = NULL;

    unique_ptr<FileFingerprint> fp(MegaApiImpl::getFileFingerprintInternal(fingerprint));
    if (!fp)
    {
        return NULL;
    }

    SdkMutexGuard g(sdkMutex);
    sharedNode_vector nodes = client->mNodeManager.getNodesByFingerprint(*fp);
    for (auto &node : nodes)
    {
        if ((!name || !strcmp(name, node->displayname())) &&
                client->checkaccess(node.get(), OWNER))
        {
            Node *n = node.get();
            while (n)
            {
                if (n->type == RUBBISHNODE)
                {
                    node = NULL;
                    break;
                }
                n = n->parent.get();
            }

            if (!node)
            {
                continue;
            }

            result = MegaNodePrivate::fromNode(node.get());
            break;
        }
    }
    return result;
}

MegaNode *MegaApiImpl::getNodeByFingerprint(const char *fingerprint, MegaNode* parent)
{
    if(!fingerprint) return NULL;

    SdkMutexGuard g(sdkMutex);
    std::shared_ptr<Node> p;
    if(parent)
    {
        p = client->nodebyhandle(parent->getHandle());
    }

    return MegaNodePrivate::fromNode(getNodeByFingerprintInternal(fingerprint, p.get()).get());
}

bool MegaApiImpl::hasFingerprint(const char *fingerprint)
{
    return (getNodeByFingerprintInternal(fingerprint) != NULL);
}

char *MegaApiImpl::getCRC(const char *filePath)
{
    if(!filePath) return NULL;

    auto localpath = LocalPath::fromAbsolutePath(filePath);

    auto fa = fsAccess->newfileaccess();
    if (!fa->fopen(localpath, OPEN_RDONLY, FSLogging::logOnError))
        return NULL;

    FileFingerprint fp;
    fp.genfingerprint(fa.get());
    if(fp.size < 0)
        return NULL;

    string result;
    result.resize((sizeof fp.crc) * 4 / 3 + 4);
    result.resize(Base64::btoa((const byte*)fp.crc.data(), sizeof fp.crc, (char*)result.c_str()));
    return MegaApi::strdup(result.c_str());
}

char *MegaApiImpl::getCRCFromFingerprint(const char *fingerprint)
{
    unique_ptr<FileFingerprint> fp(MegaApiImpl::getFileFingerprintInternal(fingerprint));
    if (!fp)
    {
        return NULL;
    }

    string result;
    result.resize((sizeof fp->crc) * 4 / 3 + 4);
    result.resize(Base64::btoa((const byte*)fp->crc.data(), sizeof fp->crc, (char*)result.c_str()));
    return MegaApi::strdup(result.c_str());
}

char *MegaApiImpl::getCRC(MegaNode *n)
{
    if(!n) return NULL;

    SdkMutexGuard g(sdkMutex);
    std::shared_ptr<Node> node = client->nodebyhandle(n->getHandle());
    if(!node || node->type != FILENODE || node->size < 0 || !node->isvalid)
    {
        return NULL;
    }

    string result;
    result.resize((sizeof node->crc) * 4 / 3 + 4);
    result.resize(Base64::btoa((const byte*)node->crc.data(),
                               sizeof node->crc.data(),
                               (char*)result.c_str()));
    return MegaApi::strdup(result.c_str());
}

MegaNode *MegaApiImpl::getNodeByCRC(const char *crc, MegaNode *parent)
{
    if(!parent) return NULL;

    SdkMutexGuard g(sdkMutex);
    std::shared_ptr<Node> node = client->nodebyhandle(parent->getHandle());
    if(!node || node->type == FILENODE)
    {
        return NULL;
    }

    byte binarycrc[sizeof(node->crc)];
    Base64::atob(crc, binarycrc, sizeof(binarycrc));

    sharedNode_list nodeList = client->getChildren(node.get());
    for (sharedNode_list::iterator it = nodeList.begin(); it != nodeList.end(); it++)
    {
        Node *child = it->get();
        if(!memcmp(child->crc.data(), binarycrc, sizeof(node->crc)))
        {
            MegaNode *result = MegaNodePrivate::fromNode(child);
            return result;
        }
    }
    return NULL;
}

void MegaApiImpl::file_added(File *f)
{
    Transfer *t = f->transfer;
    MegaTransferPrivate *transfer = currentTransfer;
    if (!transfer)
    {
        transfer = new MegaTransferPrivate(t->type);

        transfer->setSyncTransfer(f->syncxfer);

        if (t->type == GET)
        {
            transfer->setNodeHandle(f->h.as8byte());
        }
        else
        {
            transfer->setParentHandle(f->h.as8byte());
        }

        // Extract the transfer's logical path.
        // Set the transfer's raw path.
        transfer->setLocalPath(f->logicalPath());
    }

    currentTransfer = NULL;
    transfer->setTransfer(t);
    transfer->setState(t->state);
    transfer->setPriority(t->priority);
    transfer->setTotalBytes(t->size);
    transfer->setTransferredBytes(t->progresscompleted);
    transfer->setTag(f->tag);
    transferMap[f->tag] = transfer;

    fireOnTransferStart(transfer);
}

void MegaApiImpl::file_removed(File *f, const Error &e)
{
    MegaTransferPrivate* transfer = getMegaTransferPrivate(f->tag);
    if (transfer)
    {
        processTransferRemoved(f->transfer, transfer, e);
    }
}

void MegaApiImpl::file_complete(File *f)
{
    auto* transfer = getMegaTransferPrivate(f->tag);

    if (!transfer)
        return;

    if (!f->isFuseTransfer())
    {
        if (f->transfer->type == GET)
        {
            transfer->setLocalPath(f->getLocalname());
        }
        else if (f->transfer->type == PUT && (f->getLocalname() != transfer->getLocalPath()))
        {
            LOG_debug << "[MegaApiImpl::file_complete] Changing transfer path from '"
                      << transfer->getLocalPath().toPath(false) << "' to '"
                      << f->getLocalname().toPath(false) << "'";
            transfer->setLocalPath(f->getLocalname());
        }
    }

    processTransferComplete(f->transfer, transfer);
}

void MegaApiImpl::transfer_complete(Transfer *t)
{
    MegaTransferPrivate* transfer = getMegaTransferPrivate(t->tag);
    if (transfer)
    {
        transfer->setTransfer(nullptr);
    }
}

void MegaApiImpl::transfer_removed(Transfer *t)
{
    MegaTransferPrivate* transfer = getMegaTransferPrivate(t->tag);
    if (transfer)
    {
        transfer->setTransfer(nullptr);
    }
}
void MegaApiImpl::transfer_prepare(Transfer *t)
{
    for (file_list::iterator it = t->files.begin(); it != t->files.end(); it++)
    {
        MegaTransferPrivate* transfer = getMegaTransferPrivate((*it)->tag);
        if (!transfer)
        {
            continue;
        }
        processTransferPrepare(t, transfer);
    }
}

void MegaApiImpl::transfer_update(Transfer *t)
{
    for (file_list::iterator it = t->files.begin(); it != t->files.end(); it++)
    {
        MegaTransferPrivate* transfer = getMegaTransferPrivate((*it)->tag);
        if (!transfer)
        {
            continue;
        }

        if ((*it)->getLocalname() != transfer->getLocalPath())
        {
            LOG_debug << "[MegaApiImpl::transfer_update] Changing transfer path from '"
                      << transfer->getLocalPath().toPath(false) << "' to '"
                      << (*it)->getLocalname().toPath(false) << "'";
            transfer->setLocalPath((*it)->getLocalname());
        }

        if (it == t->files.begin()
                && transfer->getUpdateTime() == Waiter::ds
                && transfer->getState() == t->state
                && transfer->getPriority() == t->priority
                && (!t->slot
                    || (t->slot->progressreported
                        && t->slot->progressreported != t->size)))
        {
            // don't send more than one callback per decisecond
            // if the state doesn't change, the priority doesn't change
            // and there isn't anything new or it's not the first
            // nor the last callback
            return;
        }

        processTransferUpdate(t, transfer);
    }
}

void MegaApiImpl::file_resume(string* d, direction_t* type, uint32_t dbid, FileResumeData& data)
{
    assert(type);
    assert(d);

    if (d->size() < sizeof(char))
    {
        LOG_err << "MegaApiImpl::file_resume - Invalid data size";
        return;
    }

    *type = (direction_t)MemAccess::get<char>(d->data());
    switch (*type)
    {
    case GET:
    {
        data.file = MegaFileGet::unserialize(d);
        break;
    }
    case PUT:
    {
        MegaFile* file = NULL;
        file = MegaFilePut::unserialize(d);
        if (!file)
        {
            break;
        }

        data.file = file;
        MegaTransferPrivate* transfer = file->getTransfer();
        data.inboxTarget = transfer->getInboxTarget();
        std::shared_ptr<Node> parent = client->nodebyhandle(transfer->getParentHandle());
        sharedNode_vector nodes = client->mNodeManager.getNodesByFingerprint(*file);
        const char *name = transfer->getFileName();

        auto removeFile = [this, &data, &transfer]()
        {
            TransferDbCommitter committer(client->tctable);
            delete data.file;
            delete transfer; // committer needed here
            // Stored to clean the Transfer
            data.sameNodeHandle.setUndef();
            data.file = NULL;
            data.parentHandle.setUndef();
        };

        if (parent && nodes.size() && name)
        {
            // Get previous node if any
            file->previousNode = client->childnodebyname(parent.get(), name, true);
            for (auto &node : nodes)
            {
                if (node->parent == parent && !strcmp(node->displayname(), name))
                {
                    // don't resume the upload if the node already exist in the target folder
                    removeFile();
                    break;
                }
                // Update data to copy remote, any element from nodes is valid
                else
                {
                    data.sameNodeHandle = node->nodeHandle();
                    data.remoteName = name;
                    data.parentHandle = parent->nodeHandle();
                }
            }
        }
        else if (!parent)
        {
            // don't resume the upload if parent node doesn't exist
            removeFile();
        }
        break;
    }
    default:
        break;
    }

    if (data.file)
    {
        data.file->dbid = dbid;
        currentTransfer = static_cast<MegaFile*>(data.file)->getTransfer();
        currentTransfer->dbid = data.file->dbid;
        waiter->notify();
    }
}

dstime MegaApiImpl::pread_failure(const Error &e, int retry, void* param, dstime timeLeft)
{
    MegaTransferPrivate *transfer = (MegaTransferPrivate *)param;

    if (!transfer)
    {
        LOG_warn << "pread_failure: transfer is invalid";
        return NEVER;
    }

    transfer->setUpdateTime(Waiter::ds);
    transfer->setDeltaSize(0);
    transfer->setSpeed(0);
    transfer->setMeanSpeed(0);
    transfer->setLastBytes(NULL);
    if (retry <= transfer->getMaxRetries() && e != API_EINCOMPLETE && !(e == API_ETOOMANY && e.hasExtraInfo()))
    {
        auto megaError = std::make_unique<MegaErrorPrivate>(e, timeLeft / 10);
        transfer->setLastError(megaError.get());
        transfer->setState(MegaTransfer::STATE_RETRYING);
        fireOnTransferTemporaryError(transfer, std::move(megaError));
        LOG_debug << "Streaming temporarily failed " << retry;
        if (retry <= 1)
        {
            return 0;
        }

        return static_cast<dstime>(1) << (retry - 1);
    }
    else
    {
        if (e && (e != API_EINCOMPLETE || (e == API_ETOOMANY && e.hasExtraInfo())))
        {
            transfer->setState(MegaTransfer::STATE_FAILED);
        }
        else
        {
            transfer->setState(MegaTransfer::STATE_COMPLETED);
        }
        fireOnTransferFinish(transfer, std::make_unique<MegaErrorPrivate>(e));
        return NEVER;
    }
}

bool MegaApiImpl::pread_data(byte *buffer, m_off_t len, m_off_t, m_off_t speed, m_off_t meanSpeed, void* param)
{
    MegaTransferPrivate *transfer = (MegaTransferPrivate *)param;
    LOG_verbose << "Read new data received from transfer: len = " << len << ", speed = " << (speed/1024) << " KB/s, meanSpeed = " << (meanSpeed/1024) << " KB/s, total transferred bytes = " << transfer->getTransferredBytes() << "";
    dstime currentTime = Waiter::ds;
    transfer->setStartTime(currentTime);
    transfer->setState(MegaTransfer::STATE_ACTIVE);
    transfer->setUpdateTime(currentTime);
    transfer->setDeltaSize(len);
    transfer->setLastBytes((char *)buffer);
    transfer->setTransferredBytes(transfer->getTransferredBytes() + len);
    transfer->setSpeed(speed);
    transfer->setMeanSpeed(meanSpeed);

    bool end = (transfer->getTransferredBytes() == transfer->getTotalBytes());
    fireOnTransferUpdate(transfer);
    if (!fireOnTransferData(transfer) || end)
    {
        LOG_debug << "[MegaApiImpl::pread_data] Finish. Transfer: " << param << ", end = " << end << " [this = " << this << "]";
        transfer->setState(end ? MegaTransfer::STATE_COMPLETED : MegaTransfer::STATE_CANCELLED);
        fireOnTransferFinish(transfer, std::make_unique<MegaErrorPrivate>(end ? API_OK : API_EINCOMPLETE));
        return false;
    }
    return true;
}

void MegaApiImpl::reportevent_result(error e)
{
    if(requestMap.find(client->restag) == requestMap.end()) return;
    MegaRequestPrivate* request = requestMap.at(client->restag);
    if(!request || (request->getType() != MegaRequest::TYPE_REPORT_EVENT)) return;

    fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e));
}

void MegaApiImpl::sessions_killed(handle, error e)
{
    if(requestMap.find(client->restag) == requestMap.end()) return;
    MegaRequestPrivate* request = requestMap.at(client->restag);
    if(!request || (request->getType() != MegaRequest::TYPE_KILL_SESSION)) return;

    fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e));
}

void MegaApiImpl::cleanrubbishbin_result(error e)
{
    if(requestMap.find(client->restag) == requestMap.end()) return;
    MegaRequestPrivate* request = requestMap.at(client->restag);
    if(!request || (request->getType() != MegaRequest::TYPE_CLEAN_RUBBISH_BIN)) return;

    fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e));
}

void MegaApiImpl::getrecoverylink_result(error e)
{
    if(requestMap.find(client->restag) == requestMap.end()) return;
    MegaRequestPrivate* request = requestMap.at(client->restag);
    if(!request || ((request->getType() != MegaRequest::TYPE_GET_RECOVERY_LINK) &&
                    (request->getType() != MegaRequest::TYPE_GET_CANCEL_LINK))) return;

    fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e));
}

void MegaApiImpl::queryrecoverylink_result(error e)
{
    if(requestMap.find(client->restag) == requestMap.end()) return;
    MegaRequestPrivate* request = requestMap.at(client->restag);
    if(!request || ((request->getType() != MegaRequest::TYPE_QUERY_RECOVERY_LINK) &&
                    (request->getType() != MegaRequest::TYPE_CONFIRM_RECOVERY_LINK) &&
                    (request->getType() != MegaRequest::TYPE_CONFIRM_CHANGE_EMAIL_LINK))) return;

    fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e));
}

void MegaApiImpl::queryrecoverylink_result(int type, const char *email, const char *ip, time_t, handle uh, const vector<string> *)
{
    if(requestMap.find(client->restag) == requestMap.end()) return;
    MegaRequestPrivate* request = requestMap.at(client->restag);
    int reqType = request->getType();
    if(!request || ((reqType != MegaRequest::TYPE_QUERY_RECOVERY_LINK) &&
                    (reqType != MegaRequest::TYPE_CONFIRM_RECOVERY_LINK) &&
                    (reqType != MegaRequest::TYPE_CONFIRM_CHANGE_EMAIL_LINK))) return;

    request->setEmail(email);
    request->setFlag(type == RECOVER_WITH_MASTERKEY);
    request->setNumber(type);   // not specified in MegaApi documentation
    request->setText(ip);       // not specified in MegaApi documentation
    request->setNodeHandle(uh); // not specified in MegaApi documentation

    if (reqType == MegaRequest::TYPE_QUERY_RECOVERY_LINK)
    {
        fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>());
        return;
    }
    else if (reqType == MegaRequest::TYPE_CONFIRM_RECOVERY_LINK)
    {
        int creqtag = client->reqtag;
        client->reqtag = client->restag;
        client->prelogin(email);
        client->reqtag = creqtag;
        return;
    }
    else if (reqType == MegaRequest::TYPE_CONFIRM_CHANGE_EMAIL_LINK)
    {
        if (type != CHANGE_EMAIL)
        {
            LOG_debug << "Unknown type of change email link";

            fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(API_EARGS));
            return;
        }

        const char* code;
        code = strstr(request->getLink(), MegaClient::verifyLinkPrefix());
        if (code)
        {
            code += strlen(MegaClient::verifyLinkPrefix());

            if (!checkPassword(request->getPassword()))
            {
                fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(API_ENOENT));
                return;
            }

            int creqtag = client->reqtag;
            client->reqtag = client->restag;
            if (client->accountversion == 1)
            {
                byte pwkey[SymmCipher::KEYLENGTH];
                client->pw_key(request->getPassword(), pwkey);
                client->confirmemaillink(code, request->getEmail(), pwkey);
            }
            else if (client->accountversion == 2)
            {
                client->confirmemaillink(code, request->getEmail(), NULL);
            }
            else
            {
                LOG_warn << "Version of account not supported";
                fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(API_EINTERNAL));
            }
            client->reqtag = creqtag;
        }
        else
        {
            fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(API_EARGS));
        }
    }
}

void MegaApiImpl::getprivatekey_result(error e, const byte *privk, const size_t len_privk)
{
    if(requestMap.find(client->restag) == requestMap.end()) return;
    MegaRequestPrivate* request = requestMap.at(client->restag);
    if(!request || (request->getType() != MegaRequest::TYPE_CONFIRM_RECOVERY_LINK && request->getType() != MegaRequest::TYPE_CHECK_RECOVERY_KEY)) return;

    if (e)
    {
        fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e));
        return;
    }

    const char *link = request->getLink();
    const char* code;
    code = strstr(link, MegaClient::recoverLinkPrefix());
    if (code)
    {
        code += strlen(MegaClient::recoverLinkPrefix());
    }
    else
    {
        fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(API_EARGS));
        return;
    }

    byte mk[SymmCipher::KEYLENGTH];
    Base64::atob(request->getPrivateKey(), mk, sizeof mk);

    // check the private RSA is valid after decryption with master key
    SymmCipher key;
    key.setkey(mk);

    byte privkbuf[AsymmCipher::MAXKEYLENGTH * 2];
    memcpy(privkbuf, privk, len_privk);
    key.ecb_decrypt(privkbuf, len_privk);

    AsymmCipher uk;
    if (!uk.setkey(AsymmCipher::PRIVKEY, privkbuf, int(len_privk)))
    {
        fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(API_EKEY));
        return;
    }

    if (request->getType() == MegaRequest::TYPE_CHECK_RECOVERY_KEY) {
        fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(API_OK));
        return;
    }

    int creqtag = client->reqtag;
    client->reqtag = client->restag;
    client->confirmrecoverylink(code, request->getEmail(), request->getPassword(), mk, request->getParamType());
    client->reqtag = creqtag;
}

void MegaApiImpl::confirmrecoverylink_result(error e)
{
    if(requestMap.find(client->restag) == requestMap.end()) return;
    MegaRequestPrivate* request = requestMap.at(client->restag);
    if(!request || (request->getType() != MegaRequest::TYPE_CONFIRM_RECOVERY_LINK)) return;

    fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e));
}

void MegaApiImpl::confirmcancellink_result(error e)
{
    if(requestMap.find(client->restag) == requestMap.end()) return;
    MegaRequestPrivate* request = requestMap.at(client->restag);
    if(!request || (request->getType() != MegaRequest::TYPE_CONFIRM_CANCEL_LINK)) return;

    fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e));
}

void MegaApiImpl::getemaillink_result(error e)
{
    if(requestMap.find(client->restag) == requestMap.end()) return;
    MegaRequestPrivate* request = requestMap.at(client->restag);
    if(!request || (request->getType() != MegaRequest::TYPE_GET_CHANGE_EMAIL_LINK)) return;

    fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e));
}

void MegaApiImpl::resendverificationemail_result(error e)
{
    auto it = requestMap.find(client->restag);
    if (it == requestMap.end()) return;
    MegaRequestPrivate *request = it->second;
    if (!request || ((request->getType() != MegaRequest::TYPE_RESEND_VERIFICATION_EMAIL))) return;

    fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e));
}

void MegaApiImpl::resetSmsVerifiedPhoneNumber_result(error e)
{
    if (requestMap.find(client->restag) == requestMap.end()) return;
    MegaRequestPrivate *request = requestMap.at(client->restag);
    if (!request || (request->getType() != MegaRequest::TYPE_RESET_SMS_VERIFIED_NUMBER)) return;

    fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e));
}

void MegaApiImpl::confirmemaillink_result(error e)
{
    if(requestMap.find(client->restag) == requestMap.end()) return;
    MegaRequestPrivate* request = requestMap.at(client->restag);
    if(!request || (request->getType() != MegaRequest::TYPE_CONFIRM_CHANGE_EMAIL_LINK)) return;

    fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e));
}

void MegaApiImpl::getversion_result(int versionCode, const char *versionString, error e)
{
    if(requestMap.find(client->restag) == requestMap.end()) return;
    MegaRequestPrivate* request = requestMap.at(client->restag);
    if(!request || (request->getType() != MegaRequest::TYPE_APP_VERSION)) return;

    if (!e)
    {
        request->setNumber(versionCode);
        request->setName(versionString);
    }

    fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e));
}

void MegaApiImpl::getlocalsslcertificate_result(m_time_t ts, string *certdata, error e)
{
    if(requestMap.find(client->restag) == requestMap.end()) return;
    MegaRequestPrivate* request = requestMap.at(client->restag);
    if(!request || (request->getType() != MegaRequest::TYPE_GET_LOCAL_SSL_CERT)) return;

    if (!e)
    {
        string result;
        const char *data = certdata->data();
        const char *enddata = certdata->data() + certdata->size();
        MegaStringMapPrivate *datamap = new MegaStringMapPrivate();
        for (int i = 0; data < enddata; i++)
        {
            result = i ? "-----BEGIN CERTIFICATE-----\n"
                       : "-----BEGIN RSA PRIVATE KEY-----\n";

            const char *end = strstr(data, ";");
            if (!end)
            {
                if (!i)
                {
                    delete datamap;
                    fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(API_EINTERNAL));
                    return;
                }
                end = enddata;
            }

            while (data < end)
            {
                int remaining = int(end - data);
                int dataSize = (remaining > 64) ? 64 : remaining;
                result.append(data, static_cast<size_t>(dataSize));
                result.append("\n");
                data += dataSize;
            }

            switch (i)
            {
                case 0:
                {
                    result.append("-----END RSA PRIVATE KEY-----\n");
                    datamap->set("key", result.c_str());
                    break;
                }
                case 1:
                {
                    result.append("-----END CERTIFICATE-----\n");
                    datamap->set("cert", result.c_str());
                    break;
                }
                default:
                {
                    result.append("-----END CERTIFICATE-----\n");
                    std::ostringstream oss;
                    oss << "intermediate_" << (i - 1);
                    datamap->set(oss.str().c_str(), result.c_str());
                    break;
                }
            }
            data++;
        }

        request->setNumber(ts);
        request->setMegaStringMap(datamap);
        delete datamap;
    }
    fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e));
}

void MegaApiImpl::getmegaachievements_result(AchievementsDetails *, error e)
{
    if(requestMap.find(client->restag) == requestMap.end()) return;
    MegaRequestPrivate* request = requestMap.at(client->restag);
    if(!request || (request->getType() != MegaRequest::TYPE_GET_ACHIEVEMENTS)) return;

    fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e));
}

void MegaApiImpl::mediadetection_ready()
{
    MegaEventPrivate *event = new MegaEventPrivate(MegaEvent::EVENT_MEDIA_INFO_READY);
    fireOnEvent(event);
}

void MegaApiImpl::storagesum_changed(int64_t newsum)
{
    MegaEventPrivate *event = new MegaEventPrivate(MegaEvent::EVENT_STORAGE_SUM_CHANGED);
    event->setNumber(newsum);
    fireOnEvent(event);
}

void MegaApiImpl::getmiscflags_result(error e)
{
    if (e == API_OK)
    {
        MegaEventPrivate *event = new MegaEventPrivate(MegaEvent::EVENT_MISC_FLAGS_READY);
        fireOnEvent(event);
    }

    if (requestMap.find(client->restag) == requestMap.end()) return;
    MegaRequestPrivate* request = requestMap.at(client->restag);
    if (!request || (request->getType() != MegaRequest::TYPE_GET_MISC_FLAGS)) return;

    fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e));
}

#ifdef ENABLE_CHAT

void MegaApiImpl::chatcreate_result(TextChat *chat, error e)
{
    if(requestMap.find(client->restag) == requestMap.end()) return;
    MegaRequestPrivate* request = requestMap.at(client->restag);
    if(!request || (request->getType() != MegaRequest::TYPE_CHAT_CREATE)) return;

    if (!e)
    {
        // encapsulate the chat in a list for the request
        textchat_map chatList;
        chatList[chat->getChatId()] = chat;

        auto megaChatList = std::make_unique<MegaTextChatListPrivate>(&chatList);
        request->setMegaTextChatList(megaChatList.get());

        if (request->getMegaScheduledMeetingList() && request->getMegaScheduledMeetingList()->size())
        {
           const map<handle/*schedId*/, std::unique_ptr<ScheduledMeeting>>& schedmap = chat->getSchedMeetings();
           if (!schedmap.empty())
           {
               if (schedmap.size() > 1)
               {
                    LOG_warn << "Scheduled meeting list contains more than 1 element for a new chatroom";
               }
               std::unique_ptr<MegaScheduledMeetingList> l(MegaScheduledMeetingList::createInstance());
               for (auto const& sm: schedmap)
               {
                   l->insert(new MegaScheduledMeetingPrivate(sm.second.get()));
               }
               request->setMegaScheduledMeetingList(l.get());
           }
        }
    }

    fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e));
}

void MegaApiImpl::chatinvite_result(error e)
{
    if(requestMap.find(client->restag) == requestMap.end()) return;
    MegaRequestPrivate* request = requestMap.at(client->restag);
    if(!request || (request->getType() != MegaRequest::TYPE_CHAT_INVITE)) return;

    fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e));
}

void MegaApiImpl::chatremove_result(error e)
{
    if(requestMap.find(client->restag) == requestMap.end()) return;
    MegaRequestPrivate* request = requestMap.at(client->restag);
    if(!request || (request->getType() != MegaRequest::TYPE_CHAT_REMOVE)) return;

    fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e));
}

void MegaApiImpl::chaturl_result(string *url, error e)
{
    if(requestMap.find(client->restag) == requestMap.end()) return;
    MegaRequestPrivate* request = requestMap.at(client->restag);
    if(!request || (request->getType() != MegaRequest::TYPE_CHAT_URL)) return;

    if (!e)
    {
        request->setLink(url->c_str());
    }

    fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e));
}

void MegaApiImpl::chatgrantaccess_result(error e)
{
    if(requestMap.find(client->restag) == requestMap.end()) return;
    MegaRequestPrivate* request = requestMap.at(client->restag);
    if(!request || (request->getType() != MegaRequest::TYPE_CHAT_GRANT_ACCESS)) return;

    fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e));
}

void MegaApiImpl::chatremoveaccess_result(error e)
{
    if(requestMap.find(client->restag) == requestMap.end()) return;
    MegaRequestPrivate* request = requestMap.at(client->restag);
    if(!request || (request->getType() != MegaRequest::TYPE_CHAT_REMOVE_ACCESS)) return;

    fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e));
}

void MegaApiImpl::chatupdatepermissions_result(error e)
{
    if(requestMap.find(client->restag) == requestMap.end()) return;
    MegaRequestPrivate* request = requestMap.at(client->restag);
    if(!request || (request->getType() != MegaRequest::TYPE_CHAT_UPDATE_PERMISSIONS)) return;

    fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e));
}

void MegaApiImpl::chattruncate_result(error e)
{
    if(requestMap.find(client->restag) == requestMap.end()) return;
    MegaRequestPrivate* request = requestMap.at(client->restag);
    if(!request || (request->getType() != MegaRequest::TYPE_CHAT_TRUNCATE)) return;

    fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e));
}

void MegaApiImpl::chatsettitle_result(error e)
{
    if(requestMap.find(client->restag) == requestMap.end()) return;
    MegaRequestPrivate* request = requestMap.at(client->restag);
    if(!request || (request->getType() != MegaRequest::TYPE_CHAT_SET_TITLE)) return;

    fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e));
}

void MegaApiImpl::chatpresenceurl_result(string *url, error e)
{
    if(requestMap.find(client->restag) == requestMap.end()) return;
    MegaRequestPrivate* request = requestMap.at(client->restag);
    if(!request || (request->getType() != MegaRequest::TYPE_CHAT_PRESENCE_URL)) return;

    if (!e)
    {
        request->setLink(url->c_str());
    }

    fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e));
}

void MegaApiImpl::registerpushnotification_result(error e)
{
    if(requestMap.find(client->restag) == requestMap.end()) return;
    MegaRequestPrivate* request = requestMap.at(client->restag);
    if(!request || (request->getType() != MegaRequest::TYPE_REGISTER_PUSH_NOTIFICATION)) return;

    fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e));
}

void MegaApiImpl::archivechat_result(error e)
{
    if(requestMap.find(client->restag) == requestMap.end()) return;
    MegaRequestPrivate* request = requestMap.at(client->restag);
    if(!request || (request->getType() != MegaRequest::TYPE_CHAT_ARCHIVE)) return;

    fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e));
}

void MegaApiImpl::setchatretentiontime_result(error e)
{
    if (requestMap.find(client->restag) == requestMap.end()) return;
    MegaRequestPrivate *request = requestMap.at(client->restag);
    if (!request || (request->getType() != MegaRequest::TYPE_SET_RETENTION_TIME)) return;

    fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e));
}

void MegaApiImpl::chats_updated(textchat_map* chats, int /*count*/)
{
    if (chats)
    {
        MegaTextChatList *chatList = new MegaTextChatListPrivate(chats);
        fireOnChatsUpdate(chatList);
        delete chatList;
    }
    else
    {
        fireOnChatsUpdate(NULL);
    }
}

void MegaApiImpl::richlinkrequest_result(string *richLink, error e)
{
    if(requestMap.find(client->restag) == requestMap.end()) return;
    MegaRequestPrivate* request = requestMap.at(client->restag);
    if(!request || (request->getType() != MegaRequest::TYPE_RICH_LINK)) return;

    if (!e)
    {
        request->setText(richLink->c_str());
    }
    fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e));
}

void MegaApiImpl::chatlink_result(handle h, error e)
{
    if(requestMap.find(client->restag) == requestMap.end()) return;
    MegaRequestPrivate* request = requestMap.at(client->restag);
    if(!request || (request->getType() != MegaRequest::TYPE_CHAT_LINK_HANDLE)) return;

    if (!e && !request->getFlag())
    {
        request->setParentHandle(h);
    }

    fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e));
}

void MegaApiImpl::chatlinkurl_result(handle chatid, int shard, string *link, string *ct, int numPeers,
                     m_time_t ts, bool meetingRoom, int chatOptions,
                     const std::vector<std::unique_ptr<ScheduledMeeting>>* smList,
                     handle callid, error e)
{
    if(requestMap.find(client->restag) == requestMap.end()) return;
    MegaRequestPrivate* request = requestMap.at(client->restag);
    if(!request || (request->getType() != MegaRequest::TYPE_CHAT_LINK_URL)) return;

    if (!e)
    {
        request->setLink(link->c_str());
        request->setAccess(shard);
        request->setParentHandle(chatid);
        request->setText(ct->c_str());
        request->setNumDetails(numPeers);
        request->setNumber(ts);
        request->setParamType(chatOptions);
        request->setFlag(meetingRoom);

        if (smList && !smList->empty())
        {
            std::unique_ptr<MegaScheduledMeetingList> l(MegaScheduledMeetingList::createInstance());
            for (auto const& sm: *smList)
            {
               l->insert(new MegaScheduledMeetingPrivate(sm.get()));
            }
            request->setMegaScheduledMeetingList(l.get());
        }

        if (callid != INVALID_HANDLE)
        {
            std::vector<MegaHandle> handleList;
            handleList.push_back(callid);
            request->setMegaHandleList(handleList);
        }
    }

    fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e));
}

void MegaApiImpl::chatlinkclose_result(error e)
{
    if(requestMap.find(client->restag) == requestMap.end()) return;
    MegaRequestPrivate* request = requestMap.at(client->restag);
    if(!request || (request->getType() != MegaRequest::TYPE_SET_PRIVATE_MODE)) return;

    fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e));
}

void MegaApiImpl::chatlinkjoin_result(error e)
{
    if(requestMap.find(client->restag) == requestMap.end()) return;
    MegaRequestPrivate* request = requestMap.at(client->restag);
    if(!request || (request->getType() != MegaRequest::TYPE_AUTOJOIN_PUBLIC_CHAT)) return;

    fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e));
}

#endif

void MegaApiImpl::folderlinkinfo_result(error e, handle owner, handle /*ph*/, string *attr, string* k, m_off_t currentSize, uint32_t numFiles, uint32_t numFolders, m_off_t versionsSize, uint32_t numVersions)
{
    MegaRequestPrivate* request = NULL;
    auto it = requestMap.find(client->restag);
    if (it == requestMap.end())
        return;
    request = it->second;
    if (!request || request->getType() != MegaRequest::TYPE_PUBLIC_LINK_INFORMATION)
        return;

    if (e == API_OK)
    {
        // Decrypt nodekey with the key of the folder link
        SymmCipher cipher;
        byte folderkey[SymmCipher::KEYLENGTH];
        Base64::atob(request->getPrivateKey(), folderkey, sizeof(folderkey));
        cipher.setkey(folderkey);
        const char *nodekeystr = k->data() + 9;    // skip the userhandle(8) and the `:`
        byte nodekey[FOLDERNODEKEYLENGTH];
        if (client->decryptkey(nodekeystr, nodekey, sizeof(nodekey), &cipher, 0, UNDEF))
        {
            // Decrypt node attributes with the nodekey
            cipher.setkey(nodekey);
            byte* buf = Node::decryptattr(&cipher, attr->c_str(), attr->size());
            if (buf)
            {
                AttrMap attrs;
                string fileName;
                string fingerprint;
                FileFingerprint ffp;
                m_time_t mtime = 0;
                Node::parseattr(buf, attrs, currentSize, mtime, fileName, fingerprint, ffp);
                fingerprint = MegaNodePrivate::addAppPrefixToFingerprint(fingerprint, ffp.size);

                // Normalize node name to UTF-8 string
                attr_map::iterator itAttribute = attrs.map.find('n');
                if (itAttribute != attrs.map.end() && !itAttribute->second.empty())
                {
                    LocalPath::utf8_normalize(&(itAttribute->second));
                    fileName = itAttribute->second.c_str();
                }

                MegaFolderInfoPrivate* folderInfo =
                    new MegaFolderInfoPrivate(static_cast<int>(numFiles),
                                              static_cast<int>(numFolders) - 1,
                                              static_cast<int>(numVersions),
                                              currentSize,
                                              versionsSize);
                request->setMegaFolderInfo(folderInfo);
                request->setParentHandle(owner);
                request->setText(fileName.c_str());

                delete folderInfo;
                delete [] buf;
            }
            else
            {
                LOG_err << "Error decrypting node attributes with decrypted nodekey";
                e = API_EKEY;
            }
        }
        else
        {
            LOG_err << "Error decrypting nodekey with folder link key";
            e = API_EKEY;
        }
    }

    fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e));
}

std::unique_ptr<MegaPushNotificationSettingsPrivate> MegaApiImpl::getMegaPushNotificationSetting()
{
    User* ownUser = client->ownuser();
    if (!ownUser)
        return nullptr;

    const UserAttribute* settingsJson = ownUser->getAttribute(ATTR_PUSH_SETTINGS);
    if (!settingsJson || settingsJson->isNotExisting())
    {
        return nullptr;
    }

    std::unique_ptr<MegaPushNotificationSettingsPrivate> pushSettings =
        std::make_unique<MegaPushNotificationSettingsPrivate>(settingsJson->value());
    if (pushSettings->isValid())
    {
        return pushSettings;
    }
    else
    {
        LOG_err << "Invalid JSON for received notification settings";
    }

    return nullptr;
}

#ifdef ENABLE_SYNC

MegaSyncPrivate* MegaApiImpl::cachedMegaSyncPrivateByBackupId(const SyncConfig& config)
{
    ensureIsMegaApiImplThread();
    if (mCachedMegaSyncPrivate && config.mBackupId == mCachedMegaSyncPrivate->getBackupId())
    {
        return mCachedMegaSyncPrivate.get();
    }

    mCachedMegaSyncPrivate.reset(new MegaSyncPrivate(config, client));
    return mCachedMegaSyncPrivate.get();
}

void MegaApiImpl::syncupdate_stateconfig(const SyncConfig& config)
{
    ensureIsMegaApiImplThread();
    mCachedMegaSyncPrivate.reset();

    mRecentlyNotifiedOverlayIconPaths.clear();
    mRecentlyRequestedOverlayIconPaths.clear();

    if (auto megaSync = cachedMegaSyncPrivateByBackupId(config))
    {
        fireOnSyncStateChanged(megaSync);
    }
}

void MegaApiImpl::syncupdate_stats(handle backupId, const PerSyncStats& stats)
{
    ensureIsMegaApiImplThread();
    MegaSyncStatsPrivate msp(backupId, stats);
    fireOnSyncStatsUpdated(&msp);
}

void MegaApiImpl::syncupdate_scanning(bool scanning)
{
    ensureIsMegaApiImplThread();
    receivedScanningStateFlag.store(scanning);
    fireOnGlobalSyncStateChanged();
}

void MegaApiImpl::syncupdate_stalled(bool stalled)
{
    ensureIsMegaApiImplThread();
    receivedStallFlag.store(stalled);
    fireOnGlobalSyncStateChanged();
}

void MegaApiImpl::syncupdate_conflicts(bool conflicts)
{
    ensureIsMegaApiImplThread();
    receivedNameConflictsFlag.store(conflicts);
    fireOnGlobalSyncStateChanged();
}

void MegaApiImpl::syncupdate_totalstalls(bool totalstalls)
{
    ensureIsMegaApiImplThread();
    receivedTotalStallsFlag.store(totalstalls);
    if (totalstalls)
    {
        fireOnGlobalSyncStateChanged();
    }
}

void MegaApiImpl::syncupdate_totalconflicts(bool totalconflicts)
{
    ensureIsMegaApiImplThread();
    receivedTotalNameConflictsFlag.store(totalconflicts);
    if (totalconflicts)
    {
        fireOnGlobalSyncStateChanged();
    }
}

void MegaApiImpl::syncupdate_syncing(bool syncing)
{
    ensureIsMegaApiImplThread();
    receivedSyncingStateFlag.store(syncing);
    fireOnGlobalSyncStateChanged();
}

void MegaApiImpl::syncupdate_treestate(const SyncConfig &config, const LocalPath& lp, treestate_t ts, nodetype_t)
{
    ensureIsMegaApiImplThread();
    mRecentlyNotifiedOverlayIconPaths.addOrUpdate(lp, ts);
    mRecentlyRequestedOverlayIconPaths.overwriteExisting(lp, ts);

    if (auto megaSync = cachedMegaSyncPrivateByBackupId(config))
    {
        string s = lp.toPath(false);  // MegaSync was changed to expect utf8 for all platforms

        fireOnFileSyncStateChanged(megaSync, &s, (int)ts);
    }
}


void MegaApiImpl::sync_removed(const SyncConfig& config)
{
    ensureIsMegaApiImplThread();
    mRecentlyNotifiedOverlayIconPaths.clear();
    mRecentlyRequestedOverlayIconPaths.clear();

    auto msp_ptr = std::make_unique<MegaSyncPrivate>(config, client);
    fireOnSyncDeleted(msp_ptr.get());
}

void MegaApiImpl::sync_added(const SyncConfig& config)
{
    ensureIsMegaApiImplThread();
    mCachedMegaSyncPrivate.reset();

    auto megaSync = cachedMegaSyncPrivateByBackupId(config);

    fireOnSyncAdded(megaSync);
}

void MegaApiImpl::syncupdate_remote_root_changed(const SyncConfig &config)
{
    ensureIsMegaApiImplThread();
    mCachedMegaSyncPrivate.reset();

    if (auto megaSync = cachedMegaSyncPrivateByBackupId(config))
    {
        fireOnSyncRemoteRootChanged(megaSync);
    }
}

void MegaApiImpl::syncs_restored(SyncError syncError)
{
    ensureIsMegaApiImplThread();
    mCachedMegaSyncPrivate.reset();
    MegaEventPrivate *event = new MegaEventPrivate(MegaEvent::EVENT_SYNCS_RESTORED);
    event->setNumber(syncError);
    fireOnEvent(event);
}

void MegaApiImpl::syncs_disabled(SyncError syncError)
{
    ensureIsMegaApiImplThread();
    mCachedMegaSyncPrivate.reset();
    MegaEventPrivate *event = new MegaEventPrivate(MegaEvent::EVENT_SYNCS_DISABLED);
    event->setNumber(syncError);
    fireOnEvent(event);
}

#endif

void MegaApiImpl::backupput_result(const Error& e, handle backupId)
{
    if (requestMap.find(client->restag) == requestMap.end()) return;
    MegaRequestPrivate* request = requestMap.at(client->restag);
    if (!request || (request->getType() != MegaRequest::TYPE_BACKUP_PUT)) return;

    request->setParentHandle(backupId);
    fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e));
}

// user addition/update (users never get deleted)
void MegaApiImpl::users_updated(User** u, int count)
{
    if(!count)
    {
        return;
    }

    MegaUserList *userList = NULL;
    if(u != NULL)
    {
        userList = new MegaUserListPrivate(u, count);
        fireOnUsersUpdate(userList);
    }
    else
    {
        fireOnUsersUpdate(NULL);
    }
    delete userList;
}

void MegaApiImpl::useralerts_updated(UserAlert::Base** b, int count)
{
    if (count)
    {
        MegaUserAlertList *userAlertList = b ? new MegaUserAlertListPrivate(b, count, client) : NULL;
        fireOnUserAlertsUpdate(userAlertList);
        delete userAlertList;
    }
}

void MegaApiImpl::account_updated()
{
    fireOnAccountUpdate();
}

void MegaApiImpl::sets_updated(Set** sets, int count)
{
    LOG_debug << "Sets updated: " << count;
    if (!count)
    {
        return;
    }

    if (sets)
    {
        unique_ptr<MegaSetListPrivate> sList(new MegaSetListPrivate(sets, count));
        fireOnSetsUpdate(sList.get());
    }
    else
    {
        fireOnSetsUpdate(nullptr);
    }
}

void MegaApiImpl::setelements_updated(SetElement** elements, int count)
{
    LOG_debug << "Elements updated: " << count;
    if (!count)
    {
        return;
    }

    if (elements)
    {
        unique_ptr<MegaSetElementListPrivate> eList(new MegaSetElementListPrivate(elements, count));
        fireOnSetElementsUpdate(eList.get());
    }
    else
    {
        fireOnSetElementsUpdate(nullptr);
    }
}

void MegaApiImpl::pcrs_updated(PendingContactRequest **r, int count)
{
    if(!count)
    {
        return;
    }

    MegaContactRequestList *requestList = NULL;
    if(r != NULL)
    {
        requestList = new MegaContactRequestListPrivate(r, count);
        fireOnContactRequestsUpdate(requestList);
    }
    else
    {
        fireOnContactRequestsUpdate(NULL);
    }
    delete requestList;
}

void MegaApiImpl::sequencetag_update(const string& seqTag)
{
    assert(threadId == std::this_thread::get_id());

    // no need for a separate MegaApiImpl::fireOnSeqTagUpdate (but mentioning it here for search purposes)
    for(set<MegaGlobalListener *>::iterator it = globalListeners.begin(); it != globalListeners.end() ;)
    {
        (*it++)->onSeqTagUpdate(api, &seqTag);
    }
}

void MegaApiImpl::unlink_result(handle h, error e)
{
    if(requestMap.find(client->restag) == requestMap.end()) return;
    MegaRequestPrivate* request = requestMap.at(client->restag);
    if(!request || ((request->getType() != MegaRequest::TYPE_REMOVE) &&
                    (request->getType() != MegaRequest::TYPE_MOVE)))
    {
        return;
    }

    if (request->getType() != MegaRequest::TYPE_MOVE)
    {
        request->setNodeHandle(h);
    }

    fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e));
}

void MegaApiImpl::unlinkversions_result(error e)
{
    if (requestMap.find(client->restag) == requestMap.end())
    {
        return;
    }

    MegaRequestPrivate* request = requestMap.at(client->restag);
    if (!request || request->getType() != MegaRequest::TYPE_REMOVE_VERSIONS)
    {
        return;
    }

    fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e));
}

void MegaApiImpl::fetchnodes_result(const Error &e)
{
    MegaRequestPrivate* request = NULL;
    if (!client->restag)
    {
        for (map<int, MegaRequestPrivate *>::iterator it = requestMap.begin(); it != requestMap.end(); it++)
        {
            if (it->second->getType() == MegaRequest::TYPE_FETCH_NODES)
            {
                request = it->second;
                break;
            }
        }
        if (!request)
        {
            request = new MegaRequestPrivate(MegaRequest::TYPE_FETCH_NODES);
            //request->performRequest not required as not put to requestQueue
        }

        if (e == API_OK)
        {
            assert(!client->mNodeManager.getRootNodeFiles().isUndef());    // is folder link fetched properly?

            request->setNodeHandle(client->getFolderLinkPublicHandle());
            if (!client->isValidFolderLink())    // is the key for the folder link invalid?
            {
                request->setFlag(true);
            }
        }

        if (!e && client->loggedin() == FULLACCOUNT && client->isNewSession)
        {
            updatePwdReminderData(false, false, false, false, true);
            client->isNewSession = false;
        }

        fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e));
        return;
    }

    if (requestMap.find(client->restag) == requestMap.end())
    {
        return;
    }
    request = requestMap.at(client->restag);
    if (!request || ((request->getType() != MegaRequest::TYPE_FETCH_NODES) &&
                    (request->getType() != MegaRequest::TYPE_CREATE_ACCOUNT)))
    {
        return;
    }

    if (request->getType() == MegaRequest::TYPE_FETCH_NODES)
    {
        if (e == API_OK)
        {
            // Sanity check for required root nodes
            switch (client->getClientType())
            {
                case MegaClient::ClientType::DEFAULT:
                {
                    break;
                }

                case MegaClient::ClientType::PASSWORD_MANAGER:
                    assert(!client->mNodeManager.getRootNodeVault().isUndef());
                    break;

                case MegaClient::ClientType::VPN:
                    // Allow Fetch nodes for VPN to start receiving Action Packets
                    break;
                default:
                {
                    LOG_err << "Fetch nodes requested for unexpected MegaApi type "
                            << static_cast<int>(client->getClientType());
                    assert(false);
                    break;
                }
            }

            request->setNodeHandle(client->getFolderLinkPublicHandle());
            if (!client->isValidFolderLink())    // is the key for the folder link invalid?
            {
                request->setFlag(true);
            }
        }

        if (!e && client->loggedin() == FULLACCOUNT && client->isNewSession)
        {
            updatePwdReminderData(false, false, false, false, true);
            client->isNewSession = false;
        }

        fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e));
    }
    else    // TYPE_CREATE_ACCOUNT
    {
        if (e != API_OK || request->getParamType() == MegaApi::RESUME_ACCOUNT)   // resuming ephemeral session
        {
            fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e));
            return;
        }
        else    // new account has been created
        {
            // set names silently...
            int creqtag = client->reqtag;
            client->reqtag = 0;
            string firstname = request->getName() ? request->getName() : "";
            if (!firstname.empty())
            {
                client->putua(ATTR_FIRSTNAME,
                              (const byte*)request->getName(),
                              static_cast<unsigned int>(strlen(request->getName())),
                              -1,
                              request->getNodeHandle(),
                              request->getAccess(),
                              request->getTransferredBytes(),
                              [](Error) {});
            }
            string lastname = request->getText() ? request->getText() : "";
            if (!lastname.empty())
            {
                client->putua(ATTR_LASTNAME,
                              (const byte*)request->getText(),
                              static_cast<unsigned int>(strlen(request->getText())),
                              -1,
                              UNDEF,
                              0,
                              0,
                              [](Error) {});
            }
            client->reqtag = creqtag;   // restore current reqtag, for future requests

            // Ephemeral++ don't have an email when account is created, so cannot send a signup link -> account is ready
            if (request->getParamType() == MegaApi::CREATE_EPLUSPLUS_ACCOUNT)   // creation of account E++
            {
                // The session id cannot follow the same pattern, since no password is provided (yet)
                // In consequence, the session resumption requires a regular session id (instead of the
                // usual one with the user's handle and the pwd cipher)
                string sid;
                client->dumpsession(sid);
                request->setPrivateKey(sid.c_str());

                fireOnRequestFinish(request,  std::make_unique<MegaErrorPrivate>(e));
                return;
            }

            // Ephemeral accounts have an email -> send signup link
            if (!request->getPrivateKey()) // ...and finally send confirmation link
            {
                string fullname = firstname + lastname;
                string derivedKey = client->sendsignuplink2(request->getEmail(), request->getPassword(), fullname.c_str(), client->restag); // Use the tag where the request belongs
                string b64derivedKey;
                Base64::btoa(derivedKey, b64derivedKey);
                request->setPrivateKey(b64derivedKey.c_str());

                char buf[SymmCipher::KEYLENGTH * 4 / 3 + 3];
                Base64::btoa((byte*) &client->me, sizeof client->me, buf);
                string sid;
                sid.append(buf);
                sid.append("#");
                Base64::btoa((byte *)derivedKey.data(), SymmCipher::KEYLENGTH, buf);
                sid.append(buf);
                request->setSessionKey(sid.c_str());
                return;
            }
        }
    }
}

void MegaApiImpl::putnodes_result(const Error& inputErr,
                                  targettype_t t,
                                  vector<NewNode>& nn,
                                  bool targetOverride,
                                  int tag,
                                  const map<string, string>& fileHandles)
{
    handle h = UNDEF;
    std::shared_ptr<Node> n = NULL;
    Error e = inputErr;

    if (!e && t != USER_HANDLE)
    {
        assert(!nn.empty() && nn.front().added && nn.front().mAddedHandle != UNDEF);
        n = client->nodebyhandle(nn.front().mAddedHandle);

        if(n)
        {
            n->applykey();
            n->setattr();
            h = n->nodehandle;
        }
    }

    MegaTransferPrivate* transfer = getMegaTransferPrivate(tag);
    if (transfer)
    {
        if (transfer->getType() == MegaTransfer::TYPE_DOWNLOAD)
        {
            return;
        }

        //scale to get the handle of the new node
        Node *ntmp;
        if (n)
        {
            handle ph = transfer->getParentHandle();
            for (ntmp = n.get(); ((ntmp->parent != NULL) && (ntmp->parent->nodehandle != ph) ); ntmp = ntmp->parent.get());
            if ((ntmp->parent != NULL) && (ntmp->parent->nodehandle == ph))
            {
                h = ntmp->nodehandle;
            }
        }

        transfer->setNodeHandle(h);
        transfer->setTargetOverride(targetOverride);
        LOG_verbose << "Set transferred bytes to transfer total bytes: " << transfer->getTotalBytes() << " (prev transferredBytes = " << transfer->getTransferredBytes() << ")";
        transfer->setTransferredBytes(transfer->getTotalBytes());

        if (!e)
        {
            transfer->setState(MegaTransfer::STATE_COMPLETED);
        }
        else
        {
            transfer->setState(MegaTransfer::STATE_FAILED);
            transfer->setForeignOverquota(e == API_EOVERQUOTA && client->isForeignNode(NodeHandle().set6byte(transfer->getParentHandle())));
        }

        fireOnTransferFinish(transfer, std::make_unique<MegaErrorPrivate>(e));
        return;
    }

    auto reqIt = requestMap.find(tag);
    MegaRequestPrivate* request = reqIt == requestMap.end() ? nullptr : reqIt->second;
    if (!request || ((request->getType() != MegaRequest::TYPE_IMPORT_LINK) &&
                     (request->getType() != MegaRequest::TYPE_CREATE_FOLDER) &&
                     (request->getType() != MegaRequest::TYPE_CREATE_PASSWORD_NODE) &&
                     (request->getType() != MegaRequest::TYPE_COPY) &&
                     (request->getType() != MegaRequest::TYPE_MOVE) &&
                     (request->getType() != MegaRequest::TYPE_RESTORE) &&
                     (request->getType() != MegaRequest::TYPE_ADD_SYNC) &&
                     (request->getType() != MegaRequest::TYPE_ADD_SYNC_PREVALIDATION) &&
                     (request->getType() != MegaRequest::TYPE_COMPLETE_BACKGROUND_UPLOAD) &&
                     (request->getType() != MegaRequest::TYPE_IMPORT_PASSWORDS_FROM_FILE)))
        return;

    if (request->getType() == MegaRequest::TYPE_COMPLETE_BACKGROUND_UPLOAD)
    {
        request->setNodeHandle(h);
        request->setFlag(targetOverride);
        fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e));
        return;
    }

    if (request->getType() == MegaRequest::TYPE_IMPORT_PASSWORDS_FROM_FILE)
    {
        if (e == API_OK)
        {
            std::vector<handle> nodeHandles;
            std::transform(nn.begin(),
                           nn.end(),
                           std::back_inserter(nodeHandles),
                           [](const NewNode& newNode)
                           {
                               assert(newNode.mAddedHandle != UNDEF);
                               return newNode.mAddedHandle;
                           });
            request->setMegaHandleList(nodeHandles);
        }
        fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e));
        return;
    }

    if (request->getType() == MegaRequest::TYPE_MOVE || request->getType() == MegaRequest::TYPE_COPY)
    {
        //scale to get the handle of the moved/copied node
        Node *ntmp;
        if (n)
        {
            for (ntmp = n.get(); ((ntmp->parent != NULL) && (ntmp->parent->nodehandle != request->getParentHandle()) ); ntmp = ntmp->parent.get());
            if ((ntmp->parent != NULL) && (ntmp->parent->nodehandle == request->getParentHandle()))
            {
                h = ntmp->nodehandle;
            }
        }
    }
    else if (request->getType() == MegaRequest::TYPE_CREATE_NODE_TREE)
    {
        request->setMegaStringMap(fileHandles);
    }

    if (request->getType() != MegaRequest::TYPE_MOVE)
    {
        request->setNodeHandle(h);
        request->setFlag(targetOverride);
        fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e));
    }
    else
    {
        if (!e)
        {
            std::shared_ptr<Node> node = client->nodebyhandle(request->getNodeHandle());
            if (!node)
            {
                e = API_ENOENT;
            }
            else
            {
                request->setNodeHandle(h);
                request->setFlag(targetOverride);
                e = client->unlink(node.get(), false, request->getTag(), false);
            }
        }

        if (e)
        {
            fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e));
        }
    }
}

void MegaApiImpl::setpcr_result(handle h, error e, opcactions_t action)
{
    if(requestMap.find(client->restag) == requestMap.end()) return;
    MegaRequestPrivate* request = requestMap.at(client->restag);
    if(!request || request->getType() != MegaRequest::TYPE_INVITE_CONTACT) return;

    if (e)
    {
        LOG_debug << "Outgoing pending contact request failed (" << MegaError::getErrorString(e) << ")";
    }
    else
    {
        switch (action)
        {
            case OPCA_DELETE:
                LOG_debug << "Outgoing pending contact request deleted successfully";
                break;
            case OPCA_REMIND:
                LOG_debug << "Outgoing pending contact request reminded successfully";
                break;
            case OPCA_ADD:
                char buffer[12];
                Base64::btoa((byte*)&h, MegaClient::PCRHANDLE, buffer);
                LOG_debug << "Outgoing pending contact request succeeded, id: " << buffer;
                break;
        }
    }

    request->setNodeHandle(h);
    request->setNumber(action);
    fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e));
}

void MegaApiImpl::updatepcr_result(error e, ipcactions_t action)
{
    if(requestMap.find(client->restag) == requestMap.end()) return;
    MegaRequestPrivate* request = requestMap.at(client->restag);
    if(!request || request->getType() != MegaRequest::TYPE_REPLY_CONTACT_REQUEST) return;

    if (e)
    {
        LOG_debug << "Incoming pending contact request update failed (" << MegaError::getErrorString(e) << ")";
    }
    else
    {
        string labels[3] = {"accepted", "denied", "ignored"};
        LOG_debug << "Incoming pending contact request successfully " << labels[(int)action];
    }

    request->setNumber(action);
    fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e));
}

void MegaApiImpl::fa_complete(handle, fatype, const char* data, uint32_t len)
{
    int tag = client->restag;
    while(tag)
    {
        if(requestMap.find(tag) == requestMap.end()) return;
        MegaRequestPrivate* request = requestMap.at(tag);
        if(!request || (request->getType() != MegaRequest::TYPE_GET_ATTR_FILE)) return;

        tag = int(request->getNumber());

        auto f = client->fsaccess->newfileaccess();
        string filePath(request->getFile());
        auto localPath = LocalPath::fromAbsolutePath(filePath);
        fsAccess->unlinklocal(localPath);

        bool success = f->fopen(localPath, OPEN_WRONLY, FSLogging::logOnError) &&
                       f->fwrite((const byte*)data, len, 0);

        f.reset();

        fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(success ? API_OK : API_EWRITE));
    }
}

int MegaApiImpl::fa_failed(handle, fatype, int retries, error e)
{
    int tag = client->restag;
    while(tag)
    {
        if(requestMap.find(tag) == requestMap.end()) return 1;
        MegaRequestPrivate* request = requestMap.at(tag);
        if(!request || (request->getType() != MegaRequest::TYPE_GET_ATTR_FILE))
            return 1;

        tag = int(request->getNumber());
        if(retries >= 2)
        {
            fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e));
        }
        else
        {
            fireOnRequestTemporaryError(request, std::make_unique<MegaErrorPrivate>(e));
        }
    }

    return (retries >= 2);
}

void MegaApiImpl::putfa_result(handle h, fatype, error e)
{
    if(requestMap.find(client->restag) == requestMap.end()) return;
    MegaRequestPrivate* request = requestMap.at(client->restag);
    if(!request || request->getType() != MegaRequest::TYPE_SET_ATTR_FILE)
        return;

    if (e == API_OK && request->getMegaBackgroundMediaUploadPtr())
    {
        request->setNodeHandle(h);
    }

    fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e));
}

void MegaApiImpl::enumeratequotaitems_result(const Product& product)
{
    if(requestMap.find(client->restag) == requestMap.end()) return;
    MegaRequestPrivate* request = requestMap.at(client->restag);
    if(!request || ((request->getType() != MegaRequest::TYPE_GET_PRICING) &&
                    (request->getType() != MegaRequest::TYPE_GET_PAYMENT_ID) &&
                    (request->getType() != MegaRequest::TYPE_UPGRADE_ACCOUNT) &&
                    (request->getType() != MegaRequest::TYPE_GET_RECOMMENDED_PRO_PLAN)))
    {
        return;
    }

    request->addProduct(product);
}

void MegaApiImpl::enumeratequotaitems_result(unique_ptr<CurrencyData> currencyData)
{
    if(requestMap.find(client->restag) == requestMap.end()) return;
    MegaRequestPrivate* request = requestMap.at(client->restag);
    if(!request || ((request->getType() != MegaRequest::TYPE_GET_PRICING) &&
                    (request->getType() != MegaRequest::TYPE_GET_PAYMENT_ID) &&
                    (request->getType() != MegaRequest::TYPE_UPGRADE_ACCOUNT) &&
                    (request->getType() != MegaRequest::TYPE_GET_RECOMMENDED_PRO_PLAN)))
    {
        return;
    }

    request->setCurrency(std::move(currencyData));
}

void MegaApiImpl::enumeratequotaitems_result(error e)
{
    if(requestMap.find(client->restag) == requestMap.end()) return;
    MegaRequestPrivate* request = requestMap.at(client->restag);
    if(!request || ((request->getType() != MegaRequest::TYPE_GET_PRICING) &&
                    (request->getType() != MegaRequest::TYPE_GET_PAYMENT_ID) &&
                    (request->getType() != MegaRequest::TYPE_UPGRADE_ACCOUNT) &&
                    (request->getType() != MegaRequest::TYPE_GET_RECOMMENDED_PRO_PLAN)))
    {
        return;
    }

    if (request->getType() == MegaRequest::TYPE_GET_RECOMMENDED_PRO_PLAN)
    {
        if (e != API_OK)
        {
            fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e));
            return;
        }
        unique_ptr<MegaAccountDetails> details(request->getMegaAccountDetails());
        unique_ptr<MegaPricing> pricing(request->getPricing());
        int recommended = calcRecommendedProLevel(*pricing.get(), *details.get());
        request->setNumber(recommended);
        fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e));
        return;
    }

    if(request->getType() == MegaRequest::TYPE_GET_PRICING)
    {
        fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e));
    }
    else
    {
        MegaPricing *pricing = request->getPricing();
        MegaCurrency *currency = request->getCurrency();
        int i;
        for(i = 0; i < pricing->getNumProducts(); i++)
        {
            if (pricing->getHandle(i) == request->getNodeHandle())
            {
                int phtype = request->getParamType();
                int64_t ts = request->getTransferredBytes();
                requestMap.erase(request->getTag());
                int nextTag = client->nextreqtag();
                request->setTag(nextTag);
                requestMap[nextTag]=request;
                client->purchase_additem(0,
                                         request->getNodeHandle(),
                                         static_cast<unsigned int>(pricing->getAmount(i)),
                                         currency->getCurrencySymbol(),
                                         0,
                                         NULL,
                                         request->getParentHandle(),
                                         phtype,
                                         ts);
                break;
            }
        }

        if (i == pricing->getNumProducts())
        {
            fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(API_ENOENT));
        }

        delete pricing;
        delete currency;
    }
}

void MegaApiImpl::additem_result(error e)
{
    if(requestMap.find(client->restag) == requestMap.end()) return;
    MegaRequestPrivate* request = requestMap.at(client->restag);
    if(!request || ((request->getType() != MegaRequest::TYPE_GET_PAYMENT_ID) &&
                    (request->getType() != MegaRequest::TYPE_UPGRADE_ACCOUNT))) return;

    if(e != API_OK)
    {
        client->purchase_begin();
        fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e));
        return;
    }

    if(request->getType() == MegaRequest::TYPE_GET_PAYMENT_ID)
    {
        char saleid[16];
        Base64::btoa((byte *)&client->purchase_basket.back(), 8, saleid);
        request->setLink(saleid);
        client->purchase_begin();
        fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(API_OK));
        return;
    }

    //MegaRequest::TYPE_UPGRADE_ACCOUNT
    int method = int(request->getNumber());

    int creqtag = client->reqtag;
    client->reqtag = client->restag;
    client->purchase_checkout(method);
    client->reqtag = creqtag;
}

void MegaApiImpl::checkout_result(const char *errortype, error e)
{
    if(requestMap.find(client->restag) == requestMap.end()) return;
    MegaRequestPrivate* request = requestMap.at(client->restag);
    if(!request || (request->getType() != MegaRequest::TYPE_UPGRADE_ACCOUNT)) return;

    if(!errortype)
    {
        fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e));
        return;
    }

    if(!strcmp(errortype, "FP"))
    {
        fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e - 100));
        return;
    }

    fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(MegaError::PAYMENT_EGENERIC));
    return;
}

void MegaApiImpl::submitpurchasereceipt_result(error e)
{
    if(requestMap.find(client->restag) == requestMap.end()) return;
    MegaRequestPrivate* request = requestMap.at(client->restag);
    if(!request || (request->getType() != MegaRequest::TYPE_SUBMIT_PURCHASE_RECEIPT)) return;

    fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e));
}

void MegaApiImpl::creditcardquerysubscriptions_result(int number, error e)
{
    if(requestMap.find(client->restag) == requestMap.end()) return;
    MegaRequestPrivate* request = requestMap.at(client->restag);
    if(!request || (request->getType() != MegaRequest::TYPE_CREDIT_CARD_QUERY_SUBSCRIPTIONS)) return;

    request->setNumber(number);
    fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e));
}

void MegaApiImpl::creditcardcancelsubscriptions_result(error e)
{
    if(requestMap.find(client->restag) == requestMap.end()) return;
    MegaRequestPrivate* request = requestMap.at(client->restag);
    if(!request || (request->getType() != MegaRequest::TYPE_CREDIT_CARD_CANCEL_SUBSCRIPTIONS)) return;

    fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e));
}
void MegaApiImpl::getpaymentmethods_result(int methods, error e)
{
    if(requestMap.find(client->restag) == requestMap.end()) return;
    MegaRequestPrivate* request = requestMap.at(client->restag);
    if(!request || (request->getType() != MegaRequest::TYPE_GET_PAYMENT_METHODS)) return;

    request->setNumber(methods);
    fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e));
}

void MegaApiImpl::userfeedbackstore_result(error e)
{
    if(requestMap.find(client->restag) == requestMap.end()) return;
    MegaRequestPrivate* request = requestMap.at(client->restag);
    if(!request || (request->getType() != MegaRequest::TYPE_SUBMIT_FEEDBACK)) return;

    fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e));
}

void MegaApiImpl::sendevent_result(error e)
{
    if(requestMap.find(client->restag) == requestMap.end()) return;
    MegaRequestPrivate* request = requestMap.at(client->restag);
    if(!request || (request->getType() != MegaRequest::TYPE_SEND_EVENT)) return;

    fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e));
}

void MegaApiImpl::supportticket_result(error e)
{
    if(requestMap.find(client->restag) == requestMap.end()) return;
    MegaRequestPrivate* request = requestMap.at(client->restag);
    if(!request || (request->getType() != MegaRequest::TYPE_SUPPORT_TICKET)) return;

    fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e));
}

void MegaApiImpl::creditcardstore_result(error e)
{
    if(requestMap.find(client->restag) == requestMap.end()) return;
    MegaRequestPrivate* request = requestMap.at(client->restag);
    if(!request || (request->getType() != MegaRequest::TYPE_CREDIT_CARD_STORE)) return;

    fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e));
}

void MegaApiImpl::copysession_result(string *session, error e)
{
    if(requestMap.find(client->restag) == requestMap.end()) return;
    MegaRequestPrivate* request = requestMap.at(client->restag);
    if(!request || (request->getType() != MegaRequest::TYPE_GET_SESSION_TRANSFER_URL)) return;

    if(e == API_OK)
    {
        const char *path = request->getText();
        string data = client->sessiontransferdata(path, session);
        data.insert(0, MegaClient::getMegaURL() + "/#sitetransfer!");

        request->setLink(data.c_str());
    }

    fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e));
}

void MegaApiImpl::clearing()
{
#ifdef ENABLE_SYNC
    ensureIsMegaApiImplThread();
    mCachedMegaSyncPrivate.reset();
#endif
}

void MegaApiImpl::notify_retry(dstime dsdelta, retryreason_t reason)
{
    if(!dsdelta)
        waitingRequest = RETRY_NONE;
    else if(dsdelta > 40)
        waitingRequest = reason;

    if (dsdelta && requestMap.size() == 1)
    {
        MegaRequestPrivate *request = requestMap.begin()->second;
        fireOnRequestTemporaryError(request, std::make_unique<MegaErrorPrivate>(API_EAGAIN, reason));
    }
}

void MegaApiImpl::notify_dbcommit()
{
    MegaEventPrivate *event = new MegaEventPrivate(MegaEvent::EVENT_COMMIT_DB);
    event->setText(client->scsn.text());
    fireOnEvent(event);
}

void MegaApiImpl::notify_storage(int storageEvent)
{
    MegaEventPrivate *event = new MegaEventPrivate(MegaEvent::EVENT_STORAGE);
    event->setNumber(storageEvent);
    fireOnEvent(event);
}

void MegaApiImpl::notify_stream_overquota(dstime timeleft)
{
    MegaEventPrivate* event = new MegaEventPrivate(MegaEvent::EVENT_STREAM_OVERQUOTA);
    event->setNumber(timeleft / 10);
    fireOnEvent(event);
}

void MegaApiImpl::notify_confirmation(const char *email)
{
    MegaEventPrivate *event = new MegaEventPrivate(MegaEvent::EVENT_ACCOUNT_CONFIRMATION);
    event->setText(email);
    fireOnEvent(event);
}

void MegaApiImpl::notify_confirm_user_email(handle user, const char* email)
{
    MegaEventPrivate* event = new MegaEventPrivate(MegaEvent::EVENT_CONFIRM_USER_EMAIL);
    event->setHandle(user);
    event->setText(email);
    fireOnEvent(event);
}

void MegaApiImpl::notify_disconnect()
{
    MegaEventPrivate *event = new MegaEventPrivate(MegaEvent::EVENT_DISCONNECT);
    fireOnEvent(event);
}

void MegaApiImpl::notify_business_status(BizStatus status)
{
    MegaEventPrivate *event = new MegaEventPrivate(MegaEvent::EVENT_BUSINESS_STATUS);
    event->setNumber(status);
    fireOnEvent(event);
}

void MegaApiImpl::http_result(error e, int httpCode, byte* data, m_off_t size)
{
    if (requestMap.find(client->restag) == requestMap.end())
    {
        return;
    }

    MegaRequestPrivate* request = requestMap.at(client->restag);
    if(!request || (request->getType() != MegaRequest::TYPE_QUERY_DNS
                    && request->getType() != MegaRequest::TYPE_CHAT_STATS
                    && request->getType() != MegaRequest::TYPE_DOWNLOAD_FILE))
    {
        return;
    }

    request->setNumber(httpCode);
    request->setTotalBytes(size);
    if (request->getType() == MegaRequest::TYPE_CHAT_STATS
            || request->getType() == MegaRequest::TYPE_QUERY_DNS)
    {
        string result;
        result.assign((const char*)data, static_cast<size_t>(size));
        request->setText(result.c_str());
    }
    else if (request->getType() == MegaRequest::TYPE_DOWNLOAD_FILE)
    {
        const char *file = request->getFile();
        if (file && e == API_OK)
        {
            auto f = client->fsaccess->newfileaccess();
            string filePath(file);
            auto localPath = LocalPath::fromAbsolutePath(filePath);

            fsAccess->unlinklocal(localPath);
            if (!f->fopen(localPath, OPEN_WRONLY, FSLogging::logOnError))
            {
                e = API_EWRITE;
            }
            else if (size)
            {
                if (!f->fwrite((const byte*)data, static_cast<unsigned int>(size), 0))
                {
                    e = API_EWRITE;
                }
            }
        }
    }
    fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e));
}


void MegaApiImpl::timer_result(error e)
{
    if (requestMap.find(client->restag) == requestMap.end())
    {
        return;
    }

    MegaRequestPrivate* request = requestMap.at(client->restag);
    if(!request || (request->getType() != MegaRequest::TYPE_TIMER))
    {
        return;
    }

    fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e));
}

void MegaApiImpl::notify_creditCardExpiry()
{
    MegaEventPrivate *event = new MegaEventPrivate(MegaEvent::EVENT_CREDIT_CARD_EXPIRY);
    fireOnEvent(event);
}

// callback for non-EAGAIN request-level errors
// retrying is futile
// this can occur e.g. with syntactically malformed requests (due to a bug) or due to an invalid application key
void MegaApiImpl::request_error(error e)
{
    // todo: shouldn't this sort of logic be part of SDK Core, rather than intermediate layer, if it is even needed?

    if (e == API_EBLOCKED && client->sid.size())
    {
        whyAmIBlocked(true);
        return;
    }

    if (e == API_ESID && client->loggingout)
    {
        // no need to panic; we caused this ourselves deliberately
        return;
    }

    MegaRequestPrivate *request = new MegaRequestPrivate(MegaRequest::TYPE_LOGOUT);
    bool keepSyncConfigsFile = true;
    request->setFlag(false);
    request->setTransferTag(keepSyncConfigsFile);
    request->setParamType(e);

    if (e == API_ESSL && client->sslfakeissuer.size())
    {
        request->setText(client->sslfakeissuer.c_str());
    }

    if (e == API_ESID)
    {
        client->locallogout(true, keepSyncConfigsFile);
    }

    request->performRequest = [this, request]()
    {
        return performRequest_logout(request);
    };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::request_response_progress(m_off_t currentProgress, m_off_t totalProgress)
{
    LOG_verbose << "Request response progress: current progress = " << currentProgress << ", total progress = " << totalProgress;
    if (!client->isFetchingNodesPendingCS())
    {
        return;
    }
    for (std::map<int,MegaRequestPrivate*>::iterator it = requestMap.begin(); it != requestMap.end(); it++)
    {
        MegaRequestPrivate *request = it->second;
        if (request && request->getType() == MegaRequest::TYPE_FETCH_NODES)
        {
            request->setTransferredBytes(currentProgress);
            if (totalProgress != -1)
            {
                request->setTotalBytes(totalProgress);
            }
            fireOnRequestUpdate(request);
        }
    }
}

void MegaApiImpl::prelogin_result(int version, string* email, string *salt, error e)
{
    if(requestMap.find(client->restag) == requestMap.end()) return;
    MegaRequestPrivate* request = requestMap.at(client->restag);
    if (!request || ((request->getType() != MegaRequest::TYPE_LOGIN)
                    && (request->getType() != MegaRequest::TYPE_CONFIRM_RECOVERY_LINK)))
    {
        return;
    }

    if (e)
    {
        fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e));
        return;
    }

    if (request->getType() == MegaRequest::TYPE_LOGIN)
    {
        const char* pin = request->getText();
        if (version == 1)
        {
            const char *password = request->getPassword();
            {
                error err;
                byte pwkey[SymmCipher::KEYLENGTH];
                err = client->pw_key(password, pwkey);
                if (err)
                {
                    fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(err));
                    return;
                }

                int creqtag = client->reqtag;
                client->reqtag = client->restag;
                client->saveV1Pwd(password); // for automatic upgrade to V2  // cannot be null by now
                client->login(email->c_str(), pwkey, pin);
                client->reqtag = creqtag;
            }
        }
        else if (version == 2 && salt)
        {
            const char *password = request->getPassword();
            {
                int creqtag = client->reqtag;
                client->reqtag = client->restag;
                client->login2(email->c_str(), password, salt, pin);
                client->reqtag = creqtag;
            }
        }
        else
        {
            fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(API_EINTERNAL));
        }
    }
    else if (request->getType() == MegaRequest::TYPE_CONFIRM_RECOVERY_LINK)
    {
        request->setParamType(version);
        const char *link = request->getLink();
        const char* code;
        const char *mk64;

        code = strstr(link, MegaClient::recoverLinkPrefix());
        if (code)
        {
            code += strlen(MegaClient::recoverLinkPrefix());
        }
        else
        {
            fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(API_EARGS));
            return;
        }

        long long type = request->getNumber();
        switch (type)
        {
            case RECOVER_WITH_MASTERKEY:
            {
                mk64 = request->getPrivateKey();
                if (!mk64)
                {
                    fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(API_EARGS));
                    return;
                }

                int creqtag = client->reqtag;
                client->reqtag = client->restag;
                client->getprivatekey(code);
                client->reqtag = creqtag;
                break;
            }
            case RECOVER_WITHOUT_MASTERKEY:
            {
                int creqtag = client->reqtag;
                client->reqtag = client->restag;
                client->confirmrecoverylink(code, email->c_str(), request->getPassword(), NULL, version);
                client->reqtag = creqtag;
                break;
            }

        default:
            LOG_debug << "Unknown type of recovery link";

            fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(API_EARGS));
            return;
        }
    }
}

// login result
void MegaApiImpl::login_result(error result)
{
    if(requestMap.find(client->restag) == requestMap.end()) return;
    MegaRequestPrivate* request = requestMap.at(client->restag);
    if(!request || (request->getType() != MegaRequest::TYPE_LOGIN && request->getType() != MegaRequest::TYPE_CREATE_ACCOUNT)) return;

    // if login with user+pwd succeed, update lastLogin timestamp
    if (result == API_OK && request->getEmail() && request->getPassword())
    {
        client->isNewSession = true;
        client->tsLogin = m_time();
    }

    fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(result));
}

void MegaApiImpl::logout_result(error e, MegaRequestPrivate* request)
{
    ensureIsMegaApiImplThread();
    if(!e || e == API_ESID)
    {
        requestMap.erase(request->getTag());

        error preverror = (error)request->getParamType();
        abortPendingActions(preverror);

        waitingRequest = RETRY_NONE;

        delete mTimezones;
        mTimezones = NULL;

#ifdef ENABLE_SYNC
        mCachedMegaSyncPrivate.reset();
        receivedStallFlag.store(false);
        receivedNameConflictsFlag.store(false);
        receivedTotalStallsFlag.store(false);
        receivedTotalNameConflictsFlag.store(false);
        receivedScanningStateFlag.store(false);
        receivedSyncingStateFlag.store(false);
        mAddressedStallFilter.clear();
#endif

        mLastReceivedLoggedInState = NOTLOGGEDIN;
        mLastReceivedLoggedInMeHandle = UNDEF;
        mLastReceivedLoggedInMyEmail.clear();
        mLastKnownRootNode.reset();
        mLastKnownVaultNode.reset();
        mLastKnownRubbishNode.reset();
    }
    fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e));
}

void MegaApiImpl::userdata_result(string* name,
                                  string* pubk,
                                  string* privk,
                                  std::vector<DiscountCode>&& discountCodes,
                                  Error result)
{
    // notify apps about the availability/update of user-flags, such as `aplvp`
    // (note that usually the API command is triggered internally, so no request is associated)
    if (result == API_OK)
    {
        MegaEventPrivate *event = new MegaEventPrivate(MegaEvent::EVENT_MISC_FLAGS_READY);
        fireOnEvent(event);
    }

    if (result == API_OK)
    {
        checkLastPurgeNotification();
    }

    if(requestMap.find(client->restag) == requestMap.end()) return;
    MegaRequestPrivate* request = requestMap.at(client->restag);
    if(!request || (request->getType() != MegaRequest::TYPE_GET_USER_DATA)) return;

    if(result == API_OK)
    {
        request->setPassword(pubk->c_str());
        request->setPrivateKey(privk->c_str());
        request->setName(name->c_str());
        request->setMegaDiscountCodes(std::move(discountCodes));
    }
    fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(result));
}

void MegaApiImpl::pubkey_result(User *u)
{
    if(requestMap.find(client->restag) == requestMap.end()) return;
    MegaRequestPrivate* request = requestMap.at(client->restag);
    if(!request || (request->getType() != MegaRequest::TYPE_GET_USER_DATA)) return;

    if(!u)
    {
        fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(API_ENOENT));
        return;
    }

    if(!u->pubk.isvalid())
    {
        fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(API_EACCESS));
        return;
    }

    string key;
    u->pubk.serializekey(&key, AsymmCipher::PUBKEY);
    char pubkbuf[AsymmCipher::MAXKEYLENGTH * 4 / 3 + 4];
    Base64::btoa((byte*)key.data(), key.size(), pubkbuf);
    request->setPassword(pubkbuf);

    char jid[16];
    Base32::btoa((byte *)&u->userhandle, MegaClient::USERHANDLE, jid);
    request->setText(jid);

    if(u->email.size())
    {
        request->setEmail(u->email.c_str());
    }

    fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(API_OK));
}

// password change result
void MegaApiImpl::changepw_result(error result)
{
    if(requestMap.find(client->restag) == requestMap.end()) return;
    MegaRequestPrivate* request = requestMap.at(client->restag);
    if(!request || request->getType() != MegaRequest::TYPE_CHANGE_PW) return;

    fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(result));
}

// the requested link could not be opened
void MegaApiImpl::openfilelink_result(const Error& result)
{
    if(requestMap.find(client->restag) == requestMap.end()) return;
    MegaRequestPrivate* request = requestMap.at(client->restag);
    if(!request || ((request->getType() != MegaRequest::TYPE_IMPORT_LINK) &&
                    (request->getType() != MegaRequest::TYPE_GET_PUBLIC_NODE))) return;

    fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(result));
}

// the requested link was opened successfully
// (it is the application's responsibility to delete n!)
void MegaApiImpl::openfilelink_result(handle ph, const byte* key, m_off_t size, string* a, string* fa, int)
{
    MegaRequestPrivate* request = NULL;
    auto it = requestMap.find(client->restag);
    if (it == requestMap.end())
        return;
    request = it->second;
    if (!request || (request->getType() != MegaRequest::TYPE_IMPORT_LINK &&
                     request->getType() != MegaRequest::TYPE_GET_PUBLIC_NODE))
        return;

    if (!client->loggedin() && (request->getType() == MegaRequest::TYPE_IMPORT_LINK))
    {
        fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(MegaError::API_EACCESS));
        return;
    }

    // no key provided --> check only that the nodehandle is valid
    if (!key && (request->getType() == MegaRequest::TYPE_GET_PUBLIC_NODE))
    {
        fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(MegaError::API_EINCOMPLETE));
        return;
    }

    AttrMap attrs;
    string fileName;
    string validName;
    string fingerprint;
    string originalfingerprint;
    FileFingerprint ffp;
    m_time_t mtime = 0;

    string attrstring;
    attrstring.resize(a->length()*4/3+4);
    attrstring.resize(Base64::btoa((const byte*)a->data(), a->length(), (char*)attrstring.data()));

    bool isNodeKeyDecrypted;
    string keystring;
    SymmCipher nodeKey;
    keystring.assign((char*)key, FILENODEKEYLENGTH);
    nodeKey.setkey(key, FILENODE);

    byte *buf = Node::decryptattr(&nodeKey, attrstring.c_str(), attrstring.size());
    if (buf)
    {
        Node::parseattr(buf, attrs, size, mtime, fileName, fingerprint, ffp);
        fingerprint = MegaNodePrivate::addAppPrefixToFingerprint(fingerprint, ffp.size);

        // Normalize node name to UTF-8 string
        attr_map::iterator itAttribute = attrs.map.find('n');
        if (itAttribute != attrs.map.end() && !itAttribute->second.empty())
        {
            LocalPath::utf8_normalize(&(itAttribute->second));
            fileName = itAttribute->second.c_str();
            validName = fileName;
        }
        delete [] buf;

        isNodeKeyDecrypted = true;
    }
    else
    {
        fileName = Node::CRYPTO_ERROR;
        request->setFlag(true);
        isNodeKeyDecrypted = false;
    }

    if (request->getType() == MegaRequest::TYPE_IMPORT_LINK)
    {
        auto parenthandle = NodeHandle().set6byte(request->getParentHandle());
        shared_ptr<Node> target = client->nodeByHandle(parenthandle);
        if (!target)
        {
            fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(MegaError::API_EARGS));
            return;
        }

        NodeHandle ovhandle;
        std::shared_ptr<Node> ovn = client->childnodebyname(target.get(), validName.c_str(), true);
        if (ovn)
        {
            if (ffp.isvalid && ovn->isvalid && ffp == *(FileFingerprint*)ovn.get())
            {
                request->setNodeHandle(ovn->nodehandle);
                fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(API_OK));
                return;
            }

            ovhandle = ovn->nodeHandle();
        }

        vector<NewNode> newnodes(1);
        auto newnode = &newnodes[0];

        // set up new node as folder node
        newnode->source = NEW_PUBLIC;
        newnode->type = FILENODE;
        newnode->nodehandle = ph;
        newnode->parenthandle = UNDEF;
        newnode->nodekey.assign((char*)key,FILENODEKEYLENGTH);
        newnode->attrstring.reset(new string(*a));
        newnode->ovhandle = ovhandle;

        // add node
        requestMap.erase(request->getTag());
        int nextTag = client->nextreqtag();
        request->setTag(nextTag);
        requestMap[nextTag]=request;
        Pitag pitag{PitagPurpose::Import,
                    PitagTrigger::NotApplicable,
                    PitagNodeType::File,
                    PitagTarget::CloudDrive,
                    PitagImportSource::FileLink};
        if (target->matchesOrHasAncestorMatching(
                [](const Node& node)
                {
                    return node.inshare != nullptr;
                }))
        {
            pitag.target = PitagTarget::IncomingShare;
        }
        client->putnodes(parenthandle,
                         UseLocalVersioningFlag,
                         std::move(newnodes),
                         nullptr,
                         nextTag,
                         false,
                         {}, // customerIpPort
                         nullptr,
                         pitag);
    }
    else
    {
        MegaNodePrivate *megaNodePrivate = new MegaNodePrivate(fileName.c_str(), FILENODE, size, 0, mtime, ph, &keystring,
                                                           fa, fingerprint.size() ? fingerprint.c_str() : NULL,
                                                           originalfingerprint.size() ? originalfingerprint.c_str() : NULL, INVALID_HANDLE, INVALID_HANDLE,
                                                               nullptr, nullptr, true, false, nullptr, isNodeKeyDecrypted);
        request->setPublicNode(megaNodePrivate);
        delete megaNodePrivate;
        fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(MegaError::API_OK));
    }
}

// it may need a full reload, depending on the reason of the error
void MegaApiImpl::notifyError(const char* reason, ErrorReason errorReason)
{
    MegaEventPrivate *event = new MegaEventPrivate(MegaEvent::EVENT_FATAL_ERROR);
    event->setText(reason);
    event->setNumber(static_cast<int64_t>(errorReason));

    fireOnEvent(event);
}

void MegaApiImpl::reloading()
{
    MegaEventPrivate *event = new MegaEventPrivate(MegaEvent::EVENT_RELOADING);
    fireOnEvent(event);
}

// nodes have been modified
// (nodes with their removed flag set will be deleted immediately after returning from this call,
// at which point their pointers will become invalid at that point.)
void MegaApiImpl::nodes_updated(sharedNode_vector* nodes, int count)
{
    LOG_debug << "Nodes updated: " << count;
    if (!count)
    {
        return;
    }

    MegaNodeList *nodeList = NULL;
    if (nodes != NULL)
    {
        nodeList = new MegaNodeListPrivate(*nodes);
        fireOnNodesUpdate(nodeList);
    }
    else
    {
        fireOnNodesUpdate(NULL);
    }
    delete nodeList;
}

void MegaApiImpl::account_details(AccountDetails*, bool, bool, bool, bool, bool, bool)
{
    if(requestMap.find(client->restag) == requestMap.end()) return;
    MegaRequestPrivate* request = requestMap.at(client->restag);
    if(!request || (request->getType() != MegaRequest::TYPE_ACCOUNT_DETAILS && request->getType() != MegaRequest::TYPE_GET_RECOMMENDED_PRO_PLAN)) return;

    if (request->getType() == MegaRequest::TYPE_GET_RECOMMENDED_PRO_PLAN)
    {
        // only ever one message
        client->purchase_enumeratequotaitems();
        return;
    }

    long long numPending = request->getNumber();
    numPending--;
    request->setNumber(numPending);
    if(!numPending)
    {
        bool storage_requested = request->getNumDetails() & 0x01;
        if (storage_requested && !request->getAccountDetails()->storage_max)
            fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(MegaError::API_EACCESS));
        else
            fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(MegaError::API_OK));
    }
}

void MegaApiImpl::account_details(AccountDetails*, error e)
{
    if(requestMap.find(client->restag) == requestMap.end()) return;
    MegaRequestPrivate* request = requestMap.at(client->restag);
    if(!request || (request->getType() != MegaRequest::TYPE_ACCOUNT_DETAILS && request->getType() != MegaRequest::TYPE_GET_RECOMMENDED_PRO_PLAN)) return;

    fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e));
}

void MegaApiImpl::querytransferquota_result(int code)
{
    if(requestMap.find(client->restag) == requestMap.end()) return;
    MegaRequestPrivate* request = requestMap.at(client->restag);
    if(!request || (request->getType() != MegaRequest::TYPE_QUERY_TRANSFER_QUOTA)) return;

    // pre-warn about a possible overquota for codes 2 and 3, like in the webclient
    request->setFlag((code == 2 || code == 3) ? true : false);

    fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(API_OK));
}

void MegaApiImpl::removecontact_result(error e)
{
    if(requestMap.find(client->restag) == requestMap.end()) return;
    MegaRequestPrivate* request = requestMap.at(client->restag);
    if(!request || (request->getType() != MegaRequest::TYPE_REMOVE_CONTACT)) return;

    fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e));
}

void MegaApiImpl::getua_completion(error e, MegaRequestPrivate* request)
{
    // if attempted to get ^!prd attribute but not exists yet...
    if (e == API_ENOENT)
    {
        if (request->getParamType() == MegaApi::USER_ATTR_PWD_REMINDER)
        {
            if (request->getType() == MegaRequest::TYPE_SET_ATTR_USER)
            {
                string newValue;
                User::mergePwdReminderData(request->getNumDetails(), NULL, 0, &newValue);
                request->setText(newValue.c_str());

                // set the attribute using same request tag
                client->putua(ATTR_PWD_REMINDER, (byte*) newValue.data(), unsigned(newValue.size()), client->restag, UNDEF, 0, 0, [this, request](Error e)
                {
                    fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e));
                });
                return;
            }
            else if (request->getType() == MegaRequest::TYPE_GET_ATTR_USER)
            {
                m_time_t currenttime = m_time();
                if ((currenttime - client->accountsince) > User::PWD_SHOW_AFTER_ACCOUNT_AGE
                        && (currenttime - client->tsLogin) > User::PWD_SHOW_AFTER_LASTLOGIN)
                {
                    request->setFlag(true); // the password reminder dialog should be shown
                }
            }
        }
        else if (request->getParamType() == MegaApi::USER_ATTR_RICH_PREVIEWS &&
                 request->getType() == MegaRequest::TYPE_GET_ATTR_USER)
        {
            if (request->getNumDetails() == 0)  // used to check if rich-links are enabled
            {
                request->setFlag(false);
            }
            else if (request->getNumDetails() == 1) // used to check if should show warning
            {
                request->setFlag(true);
            }
        }
        else if ((request->getParamType() == MegaApi::USER_ATTR_ALIAS
                  || request->getParamType() == MegaApi::USER_ATTR_CAMERA_UPLOADS_FOLDER
                  || request->getParamType() == MegaApi::USER_ATTR_DEVICE_NAMES
                  || request->getParamType() == MegaApi::USER_ATTR_CC_PREFS
                  || request->getParamType() == MegaApi::USER_ATTR_APPS_PREFS)
                    && request->getType() == MegaRequest::TYPE_SET_ATTR_USER)
        {
            // The attribute doesn't exists so we have to create it
            string_map records;
            MegaStringMap *stringMap = request->getMegaStringMap();
            std::unique_ptr<MegaStringList> keys(stringMap->getKeys());
            attr_t type = static_cast<attr_t>(request->getParamType());
            bool keyPrefixModifier = request->getParamType() == MegaApi::USER_ATTR_DEVICE_NAMES && request->getFlag();
            string keyPrefix = User::attributePrefixInTLV(type, keyPrefixModifier);
            for (int i = 0; i < keys->size(); i++)
            {
                const char *key = keys->get(i);
                records.emplace(keyPrefix + key, Base64::atob(stringMap->get(key)));
            }

            client->putua(type,
                          std::move(records),
                          client->restag,
                          UNDEF,
                          0,
                          0,
                          [this, request](Error e)
                          {
                              fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e));
                          });
            return;
        }
        else if ((request->getType() == MegaRequest::TYPE_GET_ATTR_USER) &&
                 (request->getParamType() == MegaApi::USER_ATTR_VISIBLE_WELCOME_DIALOG ||
                  request->getParamType() == MegaApi::USER_ATTR_VISIBLE_TERMS_OF_SERVICE))
        {
            request->setFlag(true);
        }
        else if ((request->getParamType() == MegaApi::USER_ATTR_LAST_READ_NOTIFICATION ||
                  request->getParamType() == MegaApi::USER_ATTR_LAST_ACTIONED_BANNER) &&
                 request->getType() == MegaRequest::TYPE_GET_ATTR_USER)
        {
            request->setNumber(0);
        }
    }

    fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e));
}

void MegaApiImpl::getua_completion(byte* data, unsigned len, attr_t type, MegaRequestPrivate* request)
{
    error e = API_OK;
    assert(type == request->getParamType());

    if (request->getType() == MegaRequest::TYPE_SET_ATTR_USER)
    {
        static_assert(int(ATTR_PWD_REMINDER) == int(MegaApi::USER_ATTR_PWD_REMINDER), "User Attribute Enum Mismatch");
        if (int(type) == MegaApi::USER_ATTR_PWD_REMINDER)
        {
            // merge received value with updated items
            string newValue;
            bool changed = User::mergePwdReminderData(request->getNumDetails(), (const char*) data, len, &newValue);
            request->setText(newValue.data());

            if (changed)
            {
                // set the attribute using same request tag
                client->putua(ATTR_PWD_REMINDER, (byte*) newValue.data(), unsigned(newValue.size()), client->restag, UNDEF, 0, 0, [this, request](Error e)
                {
                    fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e));
                });
            }
            else
            {
                LOG_debug << "Password-reminder data not changed, already up to date";
                fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(API_OK));
            }
        }
        return;
    }

    // only for TYPE_GET_ATTR_USER
    switch (type)
    {
        case MegaApi::USER_ATTR_AVATAR:
            if (len)
            {

                auto f = client->fsaccess->newfileaccess();
                string filePath(request->getFile());
                auto localPath = LocalPath::fromAbsolutePath(filePath);

                fsAccess->unlinklocal(localPath);

                bool success = f->fopen(localPath, OPEN_WRONLY, FSLogging::logOnError) &&
                               f->fwrite((const byte*)data, len, 0);

                f.reset();

                if (!success)
                {
                    fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(API_EWRITE));
                    return;
                }
            }
            else    // no data for the avatar
            {
                fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(API_ENOENT));
                return;
            }

            break;

        // null-terminated char arrays
        case MegaApi::USER_ATTR_FIRSTNAME:
        case MegaApi::USER_ATTR_LASTNAME:
        case MegaApi::USER_ATTR_LANGUAGE:   // it's a c-string in binary format, want the plain data
        case MegaApi::USER_ATTR_PWD_REMINDER:
        case MegaApi::USER_ATTR_DISABLE_VERSIONS:
        case MegaApi::USER_ATTR_CONTACT_LINK_VERIFICATION:
        case MegaApi::USER_ATTR_NO_CALLKIT:
        case MegaApi::USER_ATTR_VISIBLE_WELCOME_DIALOG:
        case MegaApi::USER_ATTR_VISIBLE_TERMS_OF_SERVICE:
            {
                string str((const char*)data,len);
                request->setText(str.c_str());

                static_assert(int(MegaApi::USER_ATTR_DISABLE_VERSIONS) == ATTR_DISABLE_VERSIONS, "User Attribute Enum Mismatch");
                static_assert(int(MegaApi::USER_ATTR_CONTACT_LINK_VERIFICATION) == ATTR_CONTACT_LINK_VERIFICATION, "User Attribute Enum Mismatch");
                static_assert(int(MegaApi::USER_ATTR_PWD_REMINDER) == ATTR_PWD_REMINDER, "User Attribute Enum Mismatch");
                static_assert(int(MegaApi::USER_ATTR_NO_CALLKIT) == ATTR_NO_CALLKIT,
                              "User Attribute Enum Mismatch");
                static_assert(int(MegaApi::USER_ATTR_VISIBLE_WELCOME_DIALOG) == ATTR_VISIBLE_WELCOME_DIALOG, "User Attribute Enum Mismatch");
                static_assert(int(MegaApi::USER_ATTR_VISIBLE_TERMS_OF_SERVICE) == ATTR_VISIBLE_TERMS_OF_SERVICE, "User Attribute Enum Mismatch");

                if (int(type) == MegaApi::USER_ATTR_DISABLE_VERSIONS ||
                    int(type) == MegaApi::USER_ATTR_CONTACT_LINK_VERIFICATION ||
                    int(type) == MegaApi::USER_ATTR_VISIBLE_WELCOME_DIALOG ||
                    int(type) == MegaApi::USER_ATTR_VISIBLE_TERMS_OF_SERVICE)
                {
                    request->setFlag(str == "1");
                }
                else if (int(type) == MegaApi::USER_ATTR_PWD_REMINDER)
                {
                    m_time_t currenttime = m_time();
                    bool isMasterKeyExported =
                        User::getPwdReminderData(User::PWD_MK_EXPORTED, (const char*)data, len) !=
                        0;
                    bool isLogout = request->getNumber() != 0;
                    bool pwdDontShow =
                        User::getPwdReminderData(User::PWD_DONT_SHOW, (const char*)data, len) != 0;
                    if ((!isMasterKeyExported
                            && !pwdDontShow
                            && (currenttime - client->accountsince) > User::PWD_SHOW_AFTER_ACCOUNT_AGE
                            && (currenttime - User::getPwdReminderData(User::PWD_LAST_SUCCESS, (const char*)data, len)) > User::PWD_SHOW_AFTER_LASTSUCCESS
                            && (currenttime - User::getPwdReminderData(User::PWD_LAST_LOGIN, (const char*)data, len)) > User::PWD_SHOW_AFTER_LASTLOGIN
                            && (currenttime - User::getPwdReminderData(User::PWD_LAST_SKIPPED, (const char*)data, len)) > (request->getNumber() ? User::PWD_SHOW_AFTER_LASTSKIP_LOGOUT : User::PWD_SHOW_AFTER_LASTSKIP)
                            && (currenttime - client->tsLogin) > User::PWD_SHOW_AFTER_LASTLOGIN)
                            || (isLogout && !pwdDontShow))
                    {
                        request->setFlag(true); // the password reminder dialog should be shown
                    }
                    request->setAccess(isMasterKeyExported ? 1 : 0);
                }
            }
            break;

        // numbers
        case MegaApi::USER_ATTR_RUBBISH_TIME:
        case MegaApi::USER_ATTR_STORAGE_STATE:
            {
                char *endptr;
                string str((const char*)data, len);
                m_off_t value = strtoll(str.c_str(), &endptr, 10);
                if (endptr == str.c_str() || *endptr != '\0' || value == LLONG_MAX || value == LLONG_MIN)
                {
                    value = -1;
                }

                request->setNumber(value);

                static_assert(int(MegaApi::USER_ATTR_STORAGE_STATE) == ATTR_STORAGE_STATE, "User Attribute Enum Mismatch");
                if (int(type) == MegaApi::USER_ATTR_STORAGE_STATE && (value < MegaApi::STORAGE_STATE_GREEN || value > MegaApi::STORAGE_STATE_RED))
                {
                    e = API_EINTERNAL;
                }
            }
            break;
        case MegaApi::USER_ATTR_COOKIE_SETTINGS:
            {
                e = getCookieSettings_getua_result(data, len, request);
            }
            break;

        case MegaApi::USER_ATTR_PUSH_SETTINGS:
        {
            request->setMegaPushNotificationSettings(getMegaPushNotificationSetting().get());
        }
        break;

        case MegaApi::USER_ATTR_MY_BACKUPS_FOLDER:
        {
            handle h = 0;
            if (len != MegaClient::NODEHANDLE)
            {
                LOG_err << "Wrong received data size for 'My Backups' node handle: " << len << "; expected " << MegaClient::NODEHANDLE;
                assert(false);
            }
            memcpy(&h, data, MegaClient::NODEHANDLE);
            if (!client->nodebyhandle(h))
            {
                LOG_warn << "'My Backups' node was missing, or invalid handle in USER_ATTR_MY_BACKUPS_FOLDER";
                e = API_ENOENT;
            }
            else
            {
                request->setNodeHandle(h);
            }
        }
        break;

        case MegaApi::USER_ATTR_PWM_BASE:
        {
            request->setNodeHandle(client->getPasswordManagerBase().as8byte());
        }
        break;

        case MegaApi::USER_ATTR_LAST_READ_NOTIFICATION:
        {
            e = getLastReadNotification_getua_result(data, len, request);
        }
        break;

        case MegaApi::USER_ATTR_LAST_ACTIONED_BANNER:
        {
            e = getLastActionedBanner_getua_result(data, len, request);
        }
        break;

        case MegaApi::USER_ATTR_LAST_PURGE_ACKNOWLEDGED:
        {
            e = getLastPurgeAcknowledged_getua_result(data, len, request);
        }
        break;

        // byte arrays with possible nulls in the middle --> to Base64
        case MegaApi::USER_ATTR_ED25519_PUBLIC_KEY: // fall-through
        {
            if (request->getFlag()) // asking for the fingerprint
            {
                string fingerprint = AuthRing::fingerprint(string((const char*)data, len), true);
                request->setPassword(fingerprint.c_str());
                break;
            }
        }
        // fall through
        case MegaApi::USER_ATTR_CU25519_PUBLIC_KEY:
        case MegaApi::USER_ATTR_SIG_RSA_PUBLIC_KEY:
        case MegaApi::USER_ATTR_SIG_CU255_PUBLIC_KEY:
        default:
        {
            string str;
            str.resize(len * 4 / 3 + 4);
            str.resize(Base64::btoa(data, len, (char*)str.data()));
            request->setText(str.c_str());
        }
        break;
    }

    fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e));
}

void MegaApiImpl::getua_completion(unique_ptr<string_map> uaRecords,
                                   attr_t type,
                                   MegaRequestPrivate* request)
{
    error e = API_OK;
    assert(type == static_cast<attr_t>(request->getParamType()));

    if (uaRecords)
    {
        string_map& records = *uaRecords;
        if (request->getType() == MegaRequest::TYPE_SET_ATTR_USER)
        {
            const string_map *newValuesMap = static_cast<MegaStringMapPrivate*>(request->getMegaStringMap())->getMap();
            unique_ptr<string_map> prefixedValueMap;

            if (type == ATTR_DEVICE_NAMES)
            {
                // allow only unique names for Devices and Drives
                if (haveDuplicatedValues(records, *newValuesMap)) // ignores keys
                {
                    e = API_EEXIST;
                    LOG_err << "Attribute " << User::attr2string(type) << " attempted to add duplicated value (2): "
                        << Base64::atob(newValuesMap->begin()->second); // will only have a single value
                    fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e));
                    return;
                }

                if (request->getFlag()) // external drive
                {
                    string prefix = User::attributePrefixInTLV(ATTR_DEVICE_NAMES, true);
                    prefixedValueMap = std::make_unique<string_map>();
                    for_each(newValuesMap->begin(), newValuesMap->end(),
                        [&prefixedValueMap, &prefix](const string_map::value_type& a) { prefixedValueMap->emplace(prefix + a.first, a.second); });
                    newValuesMap = prefixedValueMap.get();
                }
            }

            if (User::mergeUserAttribute(type, *newValuesMap, records))
            {
                client->putua(type,
                              string_map{records},
                              client->restag,
                              UNDEF,
                              0,
                              0,
                              [this, request](Error e)
                              {
                                  fireOnRequestFinish(request,
                                                      std::make_unique<MegaErrorPrivate>(e));
                              });
            }
            else
            {
                LOG_debug << "Attribute " << User::attr2string(type) << " not changed, already up to date";
                fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e));
            }

            return;
        }   // end of get+set

        if (request->getParamType() == MegaApi::USER_ATTR_RECENT_CLEAR_TIMESTAMP)
        {
            MegaTimeStamp time = formatRecentClearTimestamp(&records);
            if (MegaClient::isValidMegaTimeStamp(time))
            {
                std::unique_ptr<MegaStringMap> stringMap(new MegaStringMapPrivate(&records, false));
                request->setMegaStringMap(stringMap.get());
                request->setNumber(time);
            }
            else
            {
                e = API_EINTERNAL;
            }
            fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e));
            return;
        }

        // TLV data usually includes byte arrays with zeros in the middle, so values
        // must be converted into Base64 strings to avoid problems
        std::unique_ptr<MegaStringMap> stringMap(new MegaStringMapPrivate(&records, true));
        request->setMegaStringMap(stringMap.get());
        switch (request->getParamType())
        {
            // prepare request params to know if a warning should show or not
            case MegaApi::USER_ATTR_RICH_PREVIEWS:
            {
                const char *num = stringMap->get("num");

                if (request->getNumDetails() == 0)  // used to check if rich-links are enabled
                {
                    if (num)
                    {
                        string sValue = num;
                        string bValue;
                        Base64::atob(sValue, bValue);
                        request->setFlag(bValue == "1");
                    }
                    else
                    {
                        request->setFlag(false);
                    }
                }
                else if (request->getNumDetails() == 1) // used to check if should show warning
                {
                    request->setFlag(!num);
                    // it doesn't matter the value, just if it exists

                    const char *value = stringMap->get("c");
                    if (value)
                    {
                        string sValue = value;
                        string bValue;
                        Base64::atob(sValue, bValue);
                        request->setNumber(atoi(bValue.c_str()));
                    }
                }
                break;
            }
            case MegaApi::USER_ATTR_CAMERA_UPLOADS_FOLDER:
            case MegaApi::USER_ATTR_MY_CHAT_FILES_FOLDER:
            {
                // If attr is CAMERA_UPLOADS_FOLDER determine if we want to retrieve primary or secondary folder
                // If attr is MY_CHAT_FILES_FOLDER, there's no secondary folder
                const char *key = request->getParamType() == MegaApi::USER_ATTR_CAMERA_UPLOADS_FOLDER
                        && request->getFlag()
                            ? "sh"
                            : "h";

                const char *value = stringMap->get(key);
                if (!value)
                {
                    e = API_ENOENT;
                    break;
                }
                else
                {
                   handle nodehandle = 0;  // make sure top two bytes are 0
                   Base64::atob(value, (byte*) &nodehandle, MegaClient::NODEHANDLE);
                   request->setNodeHandle(nodehandle);
                }
                break;
            }
            case MegaApi::USER_ATTR_ALIAS:
            {
                // If a handle was set in the request, we have to find it in the corresponding map and return it
                if (const char* h = request->getText())
                {
                    if (auto it = records.find(h); it != records.end())
                    {
                        request->setName(it->second.c_str());
                    }
                    else
                    {
                        e = API_ENOENT;
                    }
                }
                break;
            }
            case MegaApi::USER_ATTR_DEVICE_NAMES:
            {
                if (!request->getFlag() && !request->getText()) // all devices and drives
                {
                    // the list of device names is passed in the MegaStringMap of the MegaRequest
                    break;
                }

                const char* buf = nullptr;

                if (request->getFlag()) // external drive
                {
                    handle driveId = request->getNodeHandle();
                    string key = User::attributePrefixInTLV(ATTR_DEVICE_NAMES, true) + string(Base64Str<MegaClient::DRIVEHANDLE>(driveId));
                    buf = stringMap->get(key.c_str());
                }
                else if (request->getText()) // device
                {
                    buf = stringMap->get(request->getText());
                }

                if (buf)
                {
                    request->setName(Base64::atob(buf).c_str());
                }
                else
                {
                    e = API_ENOENT;
                }
                break;
            }

            default:
                break;
        }
    }

    fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e));
}

#ifdef DEBUG
void MegaApiImpl::delua_result(error e)
{
    if (requestMap.find(client->restag) == requestMap.end())
        return;
    MegaRequestPrivate* request = requestMap.at(client->restag);
    if (!request || (request->getType() != MegaRequest::TYPE_DEL_ATTR_USER))
        return;

    fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e));
}
#endif

void MegaApiImpl::senddevcommand_result(int value)
{
    if(requestMap.find(client->restag) == requestMap.end()) return;
    MegaRequestPrivate* request = requestMap.at(client->restag);
    if(!request || (request->getType() != MegaRequest::TYPE_SEND_DEV_COMMAND)) return;

    error e = static_cast<error>(value);
    std::string command = request->getName() ? request->getName() :"";
    if (!command.compare("aodq") && value > 0)
    {
        e = API_OK;
        request->setNumber(value);
    }

    fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e));
}

void MegaApiImpl::getuseremail_result(string *email, error e)
{
    if (requestMap.find(client->restag) == requestMap.end())
    {
        return;
    }
    MegaRequestPrivate* request = requestMap.at(client->restag);
    if (!request || (request->getType() != MegaRequest::TYPE_GET_USER_EMAIL))
    {
        return;
    }

    if (e == API_OK && email)
    {
        request->setEmail(email->c_str());
    }

    fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e));
    return;
}

// user attribute update notification
void MegaApiImpl::userattr_update(User*, int, const char*)
{
}

void MegaApiImpl::nodes_current()
{
    MegaEventPrivate *event = new MegaEventPrivate(MegaEvent::EVENT_NODES_CURRENT);
    fireOnEvent(event);

    // deliver any purge notification deferred because the node tree wasn't current yet
    checkLastPurgeNotification();
}

void MegaApiImpl::checkLastPurgeNotification()
{
    // Only surface an inactivity purge that carries a last-active time. The server zeroes
    // lastActiveTs to suppress (e.g. the user is active on another session) while keeping reason,
    // so reason alone isn't enough; (now - lastActiveTs) is the inactivity period the app shows.
    if (client->mLastPurge.reason != PURGE_REASON_INACTIVE || client->mLastPurge.lastActiveTs == 0)
    {
        return;
    }

    // The newer-node check needs a loaded tree; defer (without marking) until statecurrent.
    if (!(client->mLastPurge.ts > 0 && client->mLastPurge.ts != client->mLastPurgeNotifiedTs &&
          client->statecurrent))
    {
        return;
    }

    const User* u = client->ownuser();
    if (!u)
    {
        return;
    }

    // Only EXPIRED means "changed elsewhere, value pending" -> defer. Missing or
    // CACHED_NOT_EXISTING means never acknowledged -> fire (so isExpired(), not !isValid()).
    const UserAttribute* attr = u->getAttribute(ATTR_LAST_PURGE_ACKNOWLEDGED);
    if (attr && attr->isExpired())
    {
        LOG_debug << "EVENT_LAST_PURGE deferred: ^!lpack expired, awaiting reload";
        return; // don't mark; a later loaded value can re-evaluate
    }

    // Mark now (fire or suppress) so this ts is evaluated at most once per session.
    client->mLastPurgeNotifiedTs = client->mLastPurge.ts;

    // Suppress if already acknowledged on any device (^!lpack).
    if (attr && attr->isValid() && !attr->value().empty())
    {
        try
        {
            if (std::stoll(attr->value()) == client->mLastPurge.ts)
                return;
        }
        catch (...)
        {}
    }

    // Suppress if any own node (any type, under Cloud Drive / Vault / Rubbish, not in-shares) is
    // newer than the purge. Descending only into folders skips versions; the early-exit on the
    // first newer node keeps this fast in practice.
    std::vector<std::shared_ptr<Node>> stack;
    for (const NodeHandle root: {client->mNodeManager.getRootNodeFiles(),
                                 client->mNodeManager.getRootNodeVault(),
                                 client->mNodeManager.getRootNodeRubbish()})
    {
        if (std::shared_ptr<Node> n = client->nodeByHandle(root))
            stack.push_back(std::move(n));
    }
    while (!stack.empty())
    {
        const std::shared_ptr<Node> n = std::move(stack.back());
        stack.pop_back();
        if (n->ctime > client->mLastPurge.ts)
        {
            LOG_debug << "EVENT_LAST_PURGE suppressed: node newer than purge (ctime " << n->ctime
                      << " > purgeTs " << client->mLastPurge.ts << ")";
            return; // a node newer than the purge exists -> the notification is obsolete
        }
        if (n->type != FILENODE)
        {
            for (auto& child: client->mNodeManager.getChildren(n.get()))
                stack.push_back(child);
        }
    }

    MegaEventPrivate* purgeEvent = new MegaEventPrivate(MegaEvent::EVENT_LAST_PURGE);
    purgeEvent->setNumber("ts", static_cast<int64_t>(client->mLastPurge.ts));
    purgeEvent->setNumber("reason", static_cast<int64_t>(client->mLastPurge.reason));
    // Optional, present only for PURGE_REASON_INACTIVE; omit when absent so getNumber() is empty.
    if (client->mLastPurge.warningTs > 0)
        purgeEvent->setNumber("warningTs", static_cast<int64_t>(client->mLastPurge.warningTs));
    if (client->mLastPurge.lastActiveTs > 0)
        purgeEvent->setNumber("lastActiveTs",
                              static_cast<int64_t>(client->mLastPurge.lastActiveTs));
    fireOnEvent(purgeEvent);
}

void MegaApiImpl::catchup_result()
{
    // sc requests are sent sequentially, it must be the one at front and already started (tag == 1)
    MegaRequestPrivate *request = scRequestQueue.front();
    if (!request || (request->getType() != MegaRequest::TYPE_CATCHUP) || !request->getTag()) return;
    request = scRequestQueue.pop();

    fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(API_OK));

    // if there are more sc requests in the queue, send the next one
    if (scRequestQueue.front())
    {
        waiter->notify();
    }
}

void MegaApiImpl::key_modified(handle userhandle, attr_t attribute)
{
    MegaEventPrivate *event = new MegaEventPrivate(MegaEvent::EVENT_KEY_MODIFIED);
    switch (attribute)
    {
    case ATTR_CU25519_PUBK:
        event->setNumber(0);
        break;
    case ATTR_ED25519_PUBK:
        event->setNumber(1);
        break;
    case ATTR_UNKNOWN: // used internally for RSA
        event->setNumber(2);
        break;
    case ATTR_SIG_CU255_PUBK:
        event->setNumber(3);
        break;
    case ATTR_SIG_RSA_PUBK:
        event->setNumber(4);
        break;
    default:
        event->setNumber(-1);
        break;
    }
    event->setHandle(userhandle);
    fireOnEvent(event);
}

void MegaApiImpl::upgrading_security()
{
    MegaEventPrivate *event = new MegaEventPrivate(MegaEvent::EVENT_UPGRADE_SECURITY);
    fireOnEvent(event);
}

void MegaApiImpl::downgrade_attack()
{
    MegaEventPrivate *event = new MegaEventPrivate(MegaEvent::EVENT_DOWNGRADE_ATTACK);
    fireOnEvent(event);
}

void MegaApiImpl::ephemeral_result(error e)
{
    if(requestMap.find(client->restag) == requestMap.end()) return;
    MegaRequestPrivate* request = requestMap.at(client->restag);
    if(!request || ((request->getType() != MegaRequest::TYPE_CREATE_ACCOUNT))) return;

    fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e));
}

void MegaApiImpl::ephemeral_result(handle uh, const byte* pw)
{
    if(requestMap.find(client->restag) == requestMap.end()) return;
    MegaRequestPrivate* request = requestMap.at(client->restag);
    if(!request || ((request->getType() != MegaRequest::TYPE_CREATE_ACCOUNT))) return;

    // save uh and pwcipher for session resumption of ephemeral accounts
    string sid;
    if (client->loggedin() == EPHEMERALACCOUNT)
    {
        char buf[SymmCipher::KEYLENGTH * 4 / 3 + 3];
        Base64::btoa((byte*) &uh, sizeof uh, buf);
        sid.append(buf);
        sid.append("#");
        Base64::btoa(pw, SymmCipher::KEYLENGTH, buf);
        sid.append(buf);
    }
    else // ephemeral++
    {
        string session;
        client->dumpsession(session);
        sid = Base64::btoa(session);
    }
    request->setSessionKey(sid.c_str());

    // chain a fetchnodes to get waitlink for ephemeral account
    int creqtag = client->reqtag;
    client->reqtag = client->restag;
    client->fetchnodes(false, false, false);
    client->reqtag = creqtag;
}

void MegaApiImpl::cancelsignup_result(error e)
{
    auto it = requestMap.find(client->restag);
    if (it == requestMap.end()) return;
    MegaRequestPrivate *request = it->second;
    if (!request || ((request->getType() != MegaRequest::TYPE_CREATE_ACCOUNT))) return;

    fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e));
}

void MegaApiImpl::whyamiblocked_result(int code)
{
    if (requestMap.find(client->restag) == requestMap.end())
    {
        return;
    }
    MegaRequestPrivate* request = requestMap.at(client->restag);
    if (!request || ((request->getType() != MegaRequest::TYPE_WHY_AM_I_BLOCKED)))
    {
        return;
    }

    if (code <= 0)
    {
        fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(code));
    }
    else    // code > 0
    {
        string reason = "Your account was terminated due to a breach of Mega's Terms of Service, such as abuse of rights of others; sharing and/or importing illegal data; or system abuse.";

        if (code == MegaApi::ACCOUNT_BLOCKED_TOS_COPYRIGHT)
        {
            reason = "Your account has been suspended due to copyright violations. Please check your email inbox.";
        }
        else if (code == MegaApi::ACCOUNT_BLOCKED_TOS_NON_COPYRIGHT)
        {
            reason = "Your account was terminated due to a breach of MEGA's Terms of Service, such as abuse of rights of others; sharing and/or importing illegal data; or system abuse.";
        }
        else if (code == MegaApi::ACCOUNT_BLOCKED_SUBUSER_DISABLED)
        {
            reason = "Your account has been disabled by your administrator. You may contact your business account administrator for further details.";
        }
        else if (code == MegaApi::ACCOUNT_BLOCKED_SUBUSER_REMOVED)
        {
            reason = "Your account has been removed by your administrator. You may contact your business account administrator for further details.";
        }
        else if (code == MegaApi::ACCOUNT_BLOCKED_VERIFICATION_SMS)
        {
            reason = "Your account has been blocked pending verification via SMS.";
        }
        else if (code == MegaApi::ACCOUNT_BLOCKED_VERIFICATION_EMAIL)
        {
            reason = "Your account has been temporarily suspended for your safety. Please verify your email and follow its steps to unlock your account.";
        }
        //else if (code == ACCOUNT_BLOCKED_DEFAULT) --> default reason

        bool logoutAllowed = request->getFlag();
        request->setNumber(code);
        request->setText(reason.c_str());
        fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(API_OK));

        MegaEventPrivate *event = new MegaEventPrivate(MegaEvent::EVENT_ACCOUNT_BLOCKED);
        event->setNumber(code);
        event->setText(reason.c_str());
        fireOnEvent(event);

        // (don't log out if we can be unblocked by email or sms)
        if (logoutAllowed
                && code != MegaApi::ACCOUNT_BLOCKED_VERIFICATION_SMS
                && code != MegaApi::ACCOUNT_BLOCKED_VERIFICATION_EMAIL)
        {
            bool keepSyncConfigsFile = true;
            client->locallogout(true, keepSyncConfigsFile);

            MegaRequestPrivate *logoutRequest = new MegaRequestPrivate(MegaRequest::TYPE_LOGOUT);
            logoutRequest->setFlag(false);
            logoutRequest->setTransferTag(keepSyncConfigsFile);
            logoutRequest->setParamType(API_EBLOCKED);

            logoutRequest->performRequest = [this, logoutRequest]()
            {
                return performRequest_logout(logoutRequest);
            };

            requestQueue.push(logoutRequest);
            waiter->notify();
        }
    }
}

void MegaApiImpl::contactlinkcreate_result(error e, handle h)
{
    if (requestMap.find(client->restag) == requestMap.end())
    {
        return;
    }
    MegaRequestPrivate* request = requestMap.at(client->restag);
    if (!request || ((request->getType() != MegaRequest::TYPE_CONTACT_LINK_CREATE)))
    {
        return;
    }

    if (!e)
    {
        request->setNodeHandle(h);
    }
    fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e));
}

void MegaApiImpl::contactlinkquery_result(error e, handle h, string *email, string *firstname, string *lastname, string *avatar)
{
    if (requestMap.find(client->restag) == requestMap.end())
    {
        return;
    }
    MegaRequestPrivate* request = requestMap.at(client->restag);
    if (!request || ((request->getType() != MegaRequest::TYPE_CONTACT_LINK_QUERY)))
    {
        return;
    }

    if (!e)
    {
        request->setParentHandle(h);
        request->setEmail(email->c_str());
        request->setName(Base64::atob(*firstname).c_str());
        request->setText(Base64::atob(*lastname).c_str());
        request->setFile(avatar->c_str());
    }
    fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e));
}

void MegaApiImpl::contactlinkdelete_result(error e)
{
    if (requestMap.find(client->restag) == requestMap.end())
    {
        return;
    }
    MegaRequestPrivate* request = requestMap.at(client->restag);
    if (!request || ((request->getType() != MegaRequest::TYPE_CONTACT_LINK_DELETE)))
    {
        return;
    }
    fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e));
}

void MegaApiImpl::keepmealive_result(error e)
{
    if (requestMap.find(client->restag) == requestMap.end())
    {
        return;
    }

    MegaRequestPrivate* request = requestMap.at(client->restag);
    if (!request || ((request->getType() != MegaRequest::TYPE_KEEP_ME_ALIVE)))
    {
        return;
    }
    fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e));
}

void MegaApiImpl::getpsa_result(error e, int id, string *title, string *text, string *image, string *buttontext, string *buttonlink, std::string *url)
{
    if (requestMap.find(client->restag) == requestMap.end())
    {
        return;
    }

    MegaRequestPrivate* request = requestMap.at(client->restag);
    if (!request || ((request->getType() != MegaRequest::TYPE_GET_PSA)))
    {
        return;
    }

    if (!e)
    {
        request->setNumber(id);

        if (request->getFlag()) // supports URL retrieval
        {
            request->setEmail(url->c_str());
        }
        //else -> when `w` is provided, it's still possible to receive the old format
        request->setName(title->c_str());
        request->setText(text->c_str());
        request->setFile(image->c_str());
        request->setPassword(buttontext->c_str());
        request->setLink(buttonlink->c_str());
    }

    fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e));
}

void MegaApiImpl::multifactorauthsetup_result(string *code, error e)
{
    if (requestMap.find(client->restag) == requestMap.end())
    {
        return;
    }
    MegaRequestPrivate* request = requestMap.at(client->restag);
    if (!request || ((request->getType() != MegaRequest::TYPE_MULTI_FACTOR_AUTH_GET) &&
                    (request->getType() != MegaRequest::TYPE_MULTI_FACTOR_AUTH_SET)))
    {
        return;
    }

    if (request->getType() == MegaRequest::TYPE_MULTI_FACTOR_AUTH_GET && !e)
    {
        if (!code)
        {
            fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(API_EINTERNAL));
            return;
        }
        request->setText(code->c_str());
    }

    fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e));
}

void MegaApiImpl::multifactorauthcheck_result(int enabled)
{
    if (requestMap.find(client->restag) == requestMap.end())
    {
        return;
    }
    MegaRequestPrivate* request = requestMap.at(client->restag);
    if (!request || ((request->getType() != MegaRequest::TYPE_MULTI_FACTOR_AUTH_CHECK)))
    {
        return;
    }

    if (enabled < 0)
    {
        fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(enabled));
        return;
    }

    request->setFlag(enabled != 0);
    fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(API_OK));
}

void MegaApiImpl::multifactorauthdisable_result(error e)
{
    if (requestMap.find(client->restag) == requestMap.end())
    {
        return;
    }
    MegaRequestPrivate* request = requestMap.at(client->restag);
    if (!request || ((request->getType() != MegaRequest::TYPE_MULTI_FACTOR_AUTH_SET)))
    {
        return;
    }

    fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e));
}

void MegaApiImpl::fetchtimezone_result(error e, vector<std::string> *timezones, vector<int> *timezoneoffsets, int defaulttz)
{
    unique_ptr<MegaTimeZoneDetails> tzDetails;
    if (!e)
    {
        tzDetails.reset(new MegaTimeZoneDetailsPrivate(timezones, timezoneoffsets, defaulttz));

        // update the cached timezones for notifications filtering
        delete mTimezones;
        mTimezones = tzDetails->copy();
    }

    if (requestMap.find(client->restag) == requestMap.end())
    {
        return;
    }
    MegaRequestPrivate* request = requestMap.at(client->restag);
    if (!request || ((request->getType() != MegaRequest::TYPE_FETCH_TIMEZONE)))
    {
        return;
    }

    request->setTimeZoneDetails(tzDetails.get());
    fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e));
}

void MegaApiImpl::acknowledgeuseralerts_result(error e)
{
    map<int, MegaRequestPrivate *>::iterator it = requestMap.find(client->restag);
    if (it != requestMap.end())
    {
        MegaRequestPrivate* request = it->second;
        if (request && ((request->getType() == MegaRequest::TYPE_USERALERT_ACKNOWLEDGE)))
        {
            fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e));
        }
    }
}

void MegaApiImpl::smsverificationsend_result(error e)
{
    map<int, MegaRequestPrivate *>::iterator it = requestMap.find(client->restag);
    if (it != requestMap.end())
    {
        MegaRequestPrivate* request = it->second;
        if (request && ((request->getType() == MegaRequest::TYPE_SEND_SMS_VERIFICATIONCODE)))
        {
            fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e));
        }
    }
}

void MegaApiImpl::smsverificationcheck_result(error e, string* phoneNumber)
{
    map<int, MegaRequestPrivate *>::iterator it = requestMap.find(client->restag);
    if (it != requestMap.end())
    {
        MegaRequestPrivate* request = it->second;
        if (request && ((request->getType() == MegaRequest::TYPE_CHECK_SMS_VERIFICATIONCODE)))
        {
            if (e == API_OK && phoneNumber)
            {
                request->setName(phoneNumber->c_str());
            }
            fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e));
        }
    }
}

void MegaApiImpl::getcountrycallingcodes_result(error e, map<string, vector<string>>* data)
{
    auto it = requestMap.find(client->restag);
    if (it != requestMap.end())
    {
        MegaRequestPrivate* request = it->second;
        if (request && ((request->getType() == MegaRequest::TYPE_GET_COUNTRY_CALLING_CODES)))
        {
            if (data)
            {
                auto stringListMap = std::unique_ptr<MegaStringListMap>{MegaStringListMap::createInstance()};
                for (const auto& pair : *data)
                {
                    string_vector list;
                    for (const auto& value : pair.second)
                    {
                        list.emplace_back(value);
                    }
                    auto stringList = new MegaStringListPrivate(std::move(list));
                    stringListMap->set(pair.first.c_str(), stringList);
                }
                request->setMegaStringListMap(stringListMap.get());
            }
            fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e));
        }
    }
}

void MegaApiImpl::sendsignuplink_result(error e)
{
    if(requestMap.find(client->restag) == requestMap.end()) return;
    MegaRequestPrivate* request = requestMap.at(client->restag);
    if(!request || ((request->getType() != MegaRequest::TYPE_CREATE_ACCOUNT) &&
                    (request->getType() != MegaRequest::TYPE_SEND_SIGNUP_LINK))) return;

    fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e));
}

void MegaApiImpl::confirmsignuplink2_result(handle, const char *name, const char *email, error e)
{
    if (requestMap.find(client->restag) == requestMap.end()) return;
    MegaRequestPrivate* request = requestMap.at(client->restag);
    if (!request || ((request->getType() != MegaRequest::TYPE_CONFIRM_ACCOUNT) &&
                    (request->getType() != MegaRequest::TYPE_QUERY_SIGNUP_LINK))) return;

    if (!e)
    {
        assert(strcmp(email, request->getEmail()) == 0);
        assert(strcmp(name, request->getName()) == 0);
        request->setName(name);
        request->setEmail(email);
        request->setFlag(true);
    }
    fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e));
}

void MegaApiImpl::setkeypair_result(error)
{

}

void MegaApiImpl::getbanners_result(error e)
{
    auto it = requestMap.find(client->restag);

    if (it != requestMap.end() && it->second && (it->second->getType() == MegaRequest::TYPE_GET_BANNERS))
    {
        fireOnRequestFinish(it->second, std::make_unique<MegaErrorPrivate>(e));
    }
}

void MegaApiImpl::getbanners_result(vector<BannerDetails>&& banners)
{
    auto it = requestMap.find(client->restag);
    if (it == requestMap.end()) return;
    MegaRequestPrivate* request = it->second;
    if (!request || (request->getType() != MegaRequest::TYPE_GET_BANNERS)) return;

    request->setBanners(std::move(banners));

    fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(API_OK));
}

void MegaApiImpl::dismissbanner_result(error e)
{
    auto itReq = requestMap.find(client->restag);

    if (itReq != requestMap.end() && itReq->second && (itReq->second->getType() == MegaRequest::TYPE_DISMISS_BANNER))
    {
        fireOnRequestFinish(itReq->second, std::make_unique<MegaErrorPrivate>(e));
    }
}

void MegaApiImpl::reqstat_progress(int permilprogress)
{
    MegaEventPrivate* event = new MegaEventPrivate(MegaEvent::EVENT_REQSTAT_PROGRESS);
    event->setNumber(permilprogress);
    fireOnEvent(event);
}

void MegaApiImpl::addListener(MegaListener* listener)
{
    if(!listener) return;

    SdkMutexGuard g(sdkMutex);
    listeners.insert(listener);
}

void MegaApiImpl::addRequestListener(MegaRequestListener* listener)
{
    if(!listener) return;

    SdkMutexGuard g(sdkMutex);
    requestListeners.insert(listener);
}

void MegaApiImpl::addTransferListener(MegaTransferListener* listener)
{
    if(!listener) return;

    SdkMutexGuard g(sdkMutex);
    transferListeners.insert(listener);
}

void MegaApiImpl::addScheduledCopyListener(MegaScheduledCopyListener* listener)
{
    if(!listener) return;

    SdkMutexGuard g(sdkMutex);
    backupListeners.insert(listener);
}

void MegaApiImpl::addGlobalListener(MegaGlobalListener* listener)
{
    if(!listener) return;

    SdkMutexGuard g(sdkMutex);
    globalListeners.insert(listener);
}

bool MegaApiImpl::removeListener(MegaListener* listener)
{
    if(!listener) return false;

    SdkMutexGuard g(sdkMutex);

    return listeners.erase(listener) > 0;
}

bool MegaApiImpl::removeRequestListener(MegaRequestListener* listener)
{
    if(!listener) return false;

    SdkMutexGuard g(sdkMutex);

    auto removed = requestListeners.erase(listener) > 0;

    std::map<int, MegaRequestPrivate*>::iterator it = requestMap.begin();
    while(it != requestMap.end())
    {
        MegaRequestPrivate* request = it->second;
        if(request->getListener() == listener)
            request->setListener(NULL);

        it++;
    }

    requestQueue.removeListener(listener);

    return removed;
}

bool MegaApiImpl::removeTransferListener(MegaTransferListener* listener)
{
    if(!listener) return false;

    SdkMutexGuard g(sdkMutex);

    auto removed = transferListeners.erase(listener) > 0;

    std::map<int, MegaTransferPrivate*>::iterator it = transferMap.begin();
    while(it != transferMap.end())
    {
        MegaTransferPrivate* transfer = it->second;
        if(transfer->getListener() == listener)
            transfer->setListener(NULL);

        it++;
    }

    transferQueue.removeListener(listener);

    return removed;
}

bool MegaApiImpl::removeScheduledCopyListener(MegaScheduledCopyListener* listener)
{
    if(!listener) return false;

    SdkMutexGuard g(sdkMutex);

    auto removed = backupListeners.erase(listener);

    std::map<int, MegaScheduledCopyController*>::iterator it = backupsMap.begin();
    while(it != backupsMap.end())
    {
        MegaScheduledCopyController* backup = it->second;
        if(backup->getBackupListener() == listener)
            backup->setBackupListener(NULL);

        it++;
    }

    requestQueue.removeListener(listener);

    return removed > 0;
}

bool MegaApiImpl::removeGlobalListener(MegaGlobalListener* listener)
{
    if(!listener) return false;

    SdkMutexGuard g(sdkMutex);

    return globalListeners.erase(listener) > 0;
}

void MegaApiImpl::fireOnRequestStart(MegaRequestPrivate *request)
{
    assert(threadId == std::this_thread::get_id());
    LOG_info << client->clientname << "Request (" << request->getRequestString() << ") starting";
    for(set<MegaRequestListener *>::iterator it = requestListeners.begin(); it != requestListeners.end() ;)
    {
        (*it++)->onRequestStart(api, request);
    }

    for(set<MegaListener *>::iterator it = listeners.begin(); it != listeners.end() ;)
    {
        (*it++)->onRequestStart(api, request);
    }

    MegaRequestListener* listener = request->getListener();
    if(listener)
    {
        listener->onRequestStart(api, request);
    }
}

void MegaApiImpl::fireOnRequestFinish(MegaRequestPrivate* request,
                                      unique_ptr<MegaErrorPrivate> e,
                                      [[maybe_unused]] bool callbackIsFromOtherThread)
{
    assert(callbackIsFromOtherThread || threadId == std::this_thread::get_id());

    // Called from another thread (e.g. the sync or file-service worker thread).
    // Marshal the completion onto the MegaApiImpl thread via executeOnThread so
    // that all fireOnRequestFinish work happens on a single thread.
    //
    // We must NOT touch `request` here: abortPendingActions runs on the
    // MegaApiImpl thread (e.g. during logout) and may finish and delete this
    // request concurrently. We only capture the pointer by value; the marshalled
    // task re-validates it against requestMap before using it (see
    // finishRequestIfStillTracked), so a request already finished/deleted is
    // safely skipped instead of dereferenced.
    if (threadId != std::this_thread::get_id())
    {
        auto* ePtr = e.release();
        executeOnThread(std::make_shared<ExecuteOnce>(
            [this, request, ePtr]()
            {
                finishRequestIfStillTracked(request, std::unique_ptr<MegaErrorPrivate>(ePtr));
            }));
        return;
    }

    if(e->getErrorCode())
    {
        LOG_warn << (client ? client->clientname : "") << "Request (" << request->getRequestString() << ") finished with error: " << e->getErrorString();
    }
    else
    {
        LOG_info << (client ? client->clientname : "") << "Request (" << request->getRequestString() << ") finished";
    }

    for(set<MegaRequestListener *>::iterator it = requestListeners.begin(); it != requestListeners.end() ;)
    {
        (*it++)->onRequestFinish(api, request, e.get());
    }

    for(set<MegaListener *>::iterator it = listeners.begin(); it != listeners.end() ;)
    {
        (*it++)->onRequestFinish(api, request, e.get());
    }

    MegaRequestListener* listener = request->getListener();
    if(listener)
    {
        listener->onRequestFinish(api, request, e.get());
    }

    requestMap.erase(request->getTag());

    delete request;
}

void MegaApiImpl::finishRequestIfStillTracked(MegaRequestPrivate* request,
                                              unique_ptr<MegaErrorPrivate> e)
{
    // Runs on the MegaApiImpl thread (via executeOnThread), with sdkMutex held.
    assert(threadId == std::this_thread::get_id());

    // The request may already have been finished and deleted (e.g. by
    // abortPendingActions during logout) before this deferred completion runs.
    // Confirm it is still tracked before touching it - compare by pointer value
    // only, never dereference `request`, as it may be freed.
    for (const auto& entry: requestMap)
    {
        if (entry.second == request)
        {
            fireOnRequestFinish(request, std::move(e), false);
            return;
        }
    }

    // Not tracked anymore: the request was already finished/deleted. Drop this
    // stale completion.
}

void MegaApiImpl::fireOnRequestUpdate(MegaRequestPrivate *request)
{
    assert(threadId == std::this_thread::get_id());

    for(set<MegaRequestListener *>::iterator it = requestListeners.begin(); it != requestListeners.end() ;)
    {
        (*it++)->onRequestUpdate(api, request);
    }

    for(set<MegaListener *>::iterator it = listeners.begin(); it != listeners.end() ;)
    {
        (*it++)->onRequestUpdate(api, request);
    }

    MegaRequestListener* listener = request->getListener();
    if(listener)
    {
        listener->onRequestUpdate(api, request);
    }
}

void MegaApiImpl::fireOnRequestTemporaryError(MegaRequestPrivate *request, unique_ptr<MegaErrorPrivate> e)
{
    assert(threadId == std::this_thread::get_id());

    request->setNumRetry(request->getNumRetry() + 1);

    for(set<MegaRequestListener *>::iterator it = requestListeners.begin(); it != requestListeners.end() ;)
    {
        (*it++)->onRequestTemporaryError(api, request, e.get());
    }

    for(set<MegaListener *>::iterator it = listeners.begin(); it != listeners.end() ;)
    {
        (*it++)->onRequestTemporaryError(api, request, e.get());
    }

    MegaRequestListener* listener = request->getListener();
    if(listener)
    {
        listener->onRequestTemporaryError(api, request, e.get());
    }
}

void MegaApiImpl::fireOnTransferStart(MegaTransferPrivate *transfer)
{
    assert(threadId == std::this_thread::get_id());
    transfer->setNotificationNumber(++notificationNumber);

    for(set<MegaTransferListener *>::iterator it = transferListeners.begin(); it != transferListeners.end() ;)
    {
        (*it++)->onTransferStart(api, transfer);
    }

    for(set<MegaListener *>::iterator it = listeners.begin(); it != listeners.end() ;)
    {
        (*it++)->onTransferStart(api, transfer);
    }

    MegaTransferListener* listener = transfer->getListener();
    if(listener)
    {
        listener->onTransferStart(api, transfer);
    }
}

void MegaApiImpl::fireOnTransferFinish(MegaTransferPrivate *transfer, unique_ptr<MegaErrorPrivate> e)
{
    assert(threadId == std::this_thread::get_id());
    transfer->setNotificationNumber(++notificationNumber);
    transfer->setLastError(e.get());

    if(e->getErrorCode())
    {
        if (!(transfer->getState() == MegaTransfer::STATE_CANCELLED &&
              e->getErrorCode() == API_EINCOMPLETE &&
              transfer->getFolderTransferTag() > 0))
        {
            // avoid logging large numbers of lines for cancelled folder transfers with huge numbers of sub-transfers

            LOG_warn << "Transfer (" << transfer->getTransferString() << ") finished with error: " << e->getErrorString()
                        << " File: " << transfer->getFileName();

            if (e->hasExtraInfo() && e->getErrorCode() == API_ETOOMANY)
            {
                LOG_warn << "ETD affected: user status: " << e->getUserStatus() << "  link status: " << e->getLinkStatus();
            }
        }
    }
    else
    {
        LOG_info << "Transfer (" << transfer->getTransferString() << ") finished. File: " << transfer->getFileName();
    }

    if (transfer->getType() == MegaTransfer::TYPE_UPLOAD && transfer->isSourceFileTemporary() &&
        transfer->fingerprint_filetype == FILENODE && transfer->getPath() &&
        (transfer->getState() == MegaTransfer::STATE_COMPLETED ||
         transfer->getState() == MegaTransfer::STATE_CANCELLED ||
         transfer->getState() == MegaTransfer::STATE_FAILED))
    {
        const auto wLocalPath = transfer->getLocalPath();
        bool fileRemoved = !client->fsaccess->fileExistsAt(wLocalPath);
        if (!fileRemoved)
        {
            // This prevents that file is tried to be removed more than once.
            // Anyway in case that happens, FileSystemAccess::transient_error wouldn't be set true
            fileRemoved = client->fsaccess->unlinklocal(wLocalPath);
            if (!fileRemoved)
            {
                LOG_err << "fireOnTransferFinish (TYPE_UPLOAD): cannot remove temporary local file "
                        << wLocalPath;
            }
        }
        transfer->setStage(fileRemoved);
    }

    for(set<MegaTransferListener *>::iterator it = transferListeners.begin(); it != transferListeners.end() ;)
    {
        (*it++)->onTransferFinish(api, transfer, e.get());
    }

    for(set<MegaListener *>::iterator it = listeners.begin(); it != listeners.end() ;)
    {
        (*it++)->onTransferFinish(api, transfer, e.get());
    }

    MegaTransferListener* listener = transfer->getListener();
    if (listener)
    {
        listener->onTransferFinish(api, transfer, e.get());
    }

    transferMap.erase(transfer->getTag());

    if (transfer->isStreamingTransfer())
    {
        client->removeAppData(transfer);
    }
    delete transfer;
}

void MegaApiImpl::fireOnTransferTemporaryError(MegaTransferPrivate *transfer, unique_ptr<MegaErrorPrivate> e)
{
    assert(threadId == std::this_thread::get_id());
    transfer->setNotificationNumber(++notificationNumber);

    transfer->setNumRetry(transfer->getNumRetry() + 1);

    for(set<MegaTransferListener *>::iterator it = transferListeners.begin(); it != transferListeners.end() ;)
    {
        (*it++)->onTransferTemporaryError(api, transfer, e.get());
    }

    for(set<MegaListener *>::iterator it = listeners.begin(); it != listeners.end() ;)
    {
        (*it++)->onTransferTemporaryError(api, transfer, e.get());
    }

    MegaTransferListener* listener = transfer->getListener();
    if(listener)
    {
        listener->onTransferTemporaryError(api, transfer, e.get());
    }
}

MegaClient *MegaApiImpl::getMegaClient()
{
    return client;
}

void MegaApiImpl::fireOnTransferUpdate(MegaTransferPrivate *transfer)
{
    assert(threadId == std::this_thread::get_id());
    transfer->setNotificationNumber(++notificationNumber);

    for(set<MegaTransferListener *>::iterator it = transferListeners.begin(); it != transferListeners.end() ;)
    {
        (*it++)->onTransferUpdate(api, transfer);
    }

    for(set<MegaListener *>::iterator it = listeners.begin(); it != listeners.end() ;)
    {
        (*it++)->onTransferUpdate(api, transfer);
    }

    MegaTransferListener* listener = transfer->getListener();
    if(listener)
    {
        listener->onTransferUpdate(api, transfer);
    }
}

void MegaApiImpl::fireOnFolderTransferUpdate(MegaTransferPrivate *transfer, int stage, uint32_t foldercount, uint32_t createdfoldercount, uint32_t filecount, const LocalPath* currentFolder, const LocalPath* currentFileLeafname)
{
    // this occurs on worker thread for scanning stage (for uploads) and create tree (for downloads), and on SDK thread for the rest of calls
    assert((threadId != std::this_thread::get_id()
                && ((stage == MegaTransfer::STAGE_SCAN && transfer->getType() == MegaTransfer::TYPE_UPLOAD)
                        || (stage == MegaTransfer::STAGE_CREATE_TREE && transfer->getType() == MegaTransfer::TYPE_DOWNLOAD)))
            || threadId == std::this_thread::get_id());

    transfer->setNotificationNumber(++notificationNumber);

    // This one is defined to only be called back on the listener for the transfer
    // not any of the global or megaapi listeners
    if (MegaTransferListener* listener = transfer->getListener())
    {
        listener->onFolderTransferUpdate(api, transfer, stage, foldercount, createdfoldercount, filecount,
                    currentFolder ? currentFolder->toPath(false).c_str() : nullptr,
                    currentFileLeafname ? currentFileLeafname->toPath(false).c_str() : nullptr);
    }
}

bool MegaApiImpl::fireOnTransferData(MegaTransferPrivate *transfer)
{
    assert(threadId == std::this_thread::get_id());
    transfer->setNotificationNumber(++notificationNumber);

    bool result = false;
    MegaTransferListener* listener = transfer->getListener();
    if(listener)
    {
        result = listener->onTransferData(api, transfer, transfer->getLastBytes(), size_t(transfer->getDeltaSize()));
    }
    return result;
}

void MegaApiImpl::fireOnUsersUpdate(MegaUserList *users)
{
    assert(threadId == std::this_thread::get_id());

    for(set<MegaGlobalListener *>::iterator it = globalListeners.begin(); it != globalListeners.end() ;)
    {
        (*it++)->onUsersUpdate(api, users);
    }
    for(set<MegaListener *>::iterator it = listeners.begin(); it != listeners.end() ;)
    {
        (*it++)->onUsersUpdate(api, users);
    }
}

void MegaApiImpl::fireOnUserAlertsUpdate(MegaUserAlertList *userAlerts)
{
    assert(threadId == std::this_thread::get_id());

    for(set<MegaGlobalListener *>::iterator it = globalListeners.begin(); it != globalListeners.end() ;)
    {
        (*it++)->onUserAlertsUpdate(api, userAlerts);
    }
    for (set<MegaListener *>::iterator it = listeners.begin(); it != listeners.end();)
    {
        (*it++)->onUserAlertsUpdate(api, userAlerts);
    }
}

void MegaApiImpl::fireOnContactRequestsUpdate(MegaContactRequestList *requests)
{
    assert(threadId == std::this_thread::get_id());

    for(set<MegaGlobalListener *>::iterator it = globalListeners.begin(); it != globalListeners.end() ;)
    {
        (*it++)->onContactRequestsUpdate(api, requests);
    }
    for(set<MegaListener *>::iterator it = listeners.begin(); it != listeners.end() ;)
    {
        (*it++)->onContactRequestsUpdate(api, requests);
    }
}

void MegaApiImpl::fireOnNodesUpdate(MegaNodeList *nodes)
{
    assert(threadId == std::this_thread::get_id());

    for(set<MegaGlobalListener *>::iterator it = globalListeners.begin(); it != globalListeners.end() ;)
    {
        (*it++)->onNodesUpdate(api, nodes);
    }
    for(set<MegaListener *>::iterator it = listeners.begin(); it != listeners.end() ;)
    {
        (*it++)->onNodesUpdate(api, nodes);
    }
}

void MegaApiImpl::fireOnAccountUpdate()
{
    assert(threadId == std::this_thread::get_id());
    for(set<MegaGlobalListener *>::iterator it = globalListeners.begin(); it != globalListeners.end() ;)
    {
        (*it++)->onAccountUpdate(api);
    }
    for(set<MegaListener *>::iterator it = listeners.begin(); it != listeners.end() ;)
    {
        (*it++)->onAccountUpdate(api);
    }
}

void MegaApiImpl::fireOnSetsUpdate(MegaSetList* sets)
{
    assert(threadId == std::this_thread::get_id());

    for (set<MegaGlobalListener*>::iterator it = globalListeners.begin(); it != globalListeners.end();)
    {
        (*it++)->onSetsUpdate(api, sets);
    }
    for (set<MegaListener*>::iterator it = listeners.begin(); it != listeners.end();)
    {
        (*it++)->onSetsUpdate(api, sets);
    }
}

void MegaApiImpl::fireOnSetElementsUpdate(MegaSetElementList* elements)
{
    assert(threadId == std::this_thread::get_id());

    for (set<MegaGlobalListener*>::iterator it = globalListeners.begin(); it != globalListeners.end();)
    {
        (*it++)->onSetElementsUpdate(api, elements);
    }
    for (set<MegaListener*>::iterator it = listeners.begin(); it != listeners.end();)
    {
        (*it++)->onSetElementsUpdate(api, elements);
    }
}

void MegaApiImpl::fireOnEvent(MegaEventPrivate *event)
{
    LOG_debug << "Sending " << event->getEventString() << " to app." << event->getValidDataToString();
    for(set<MegaGlobalListener *>::iterator it = globalListeners.begin(); it != globalListeners.end() ;)
    {
        (*it++)->onEvent(api, event);
    }

    for(set<MegaListener *>::iterator it = listeners.begin(); it != listeners.end() ;)
    {
        (*it++)->onEvent(api, event);
    }

    delete event;
}

#ifdef ENABLE_SYNC
void MegaApiImpl::fireOnSyncStateChanged(MegaSyncPrivate *sync)
{
    assert(sync->getBackupId() != INVALID_HANDLE);
    for(set<MegaListener *>::iterator it = listeners.begin(); it != listeners.end() ;)
    {
        (*it++)->onSyncStateChanged(api, sync);
    }
}

void MegaApiImpl::fireOnSyncStatsUpdated(MegaSyncStatsPrivate *stats)
{
    assert(stats->getBackupId() != INVALID_HANDLE);
    for(set<MegaListener *>::iterator it = listeners.begin(); it != listeners.end() ;)
    {
        (*it++)->onSyncStatsUpdated(api, stats);
    }
}

void MegaApiImpl::fireOnSyncAdded(MegaSyncPrivate *sync)
{
    assert(sync->getBackupId() != INVALID_HANDLE);
    for(set<MegaListener *>::iterator it = listeners.begin(); it != listeners.end() ;)
    {
        (*it++)->onSyncAdded(api, sync);
    }
}

void MegaApiImpl::fireOnSyncRemoteRootChanged(MegaSyncPrivate* sync)
{
    assert(sync->getBackupId() != INVALID_HANDLE);
    for (set<MegaListener*>::iterator it = listeners.begin(); it != listeners.end();)
    {
        (*it++)->onSyncRemoteRootChanged(api, sync);
    }
}

void MegaApiImpl::fireOnSyncDeleted(MegaSyncPrivate *sync)
{
    assert(sync->getBackupId() != INVALID_HANDLE);
    for(set<MegaListener *>::iterator it = listeners.begin(); it != listeners.end() ;)
    {
        (*it++)->onSyncDeleted(api, sync);
    }
}

void MegaApiImpl::fireOnGlobalSyncStateChanged()
{
    for(set<MegaListener *>::iterator it = listeners.begin(); it != listeners.end() ;)
    {
        (*it++)->onGlobalSyncStateChanged(api);
    }

    for(set<MegaGlobalListener *>::iterator it = globalListeners.begin(); it != globalListeners.end() ;)
    {
        (*it++)->onGlobalSyncStateChanged(api);
    }
}

void MegaApiImpl::fireOnFileSyncStateChanged(MegaSyncPrivate *sync, string *localPath, int newState)
{
    assert(sync->getBackupId() != INVALID_HANDLE);
    for(set<MegaListener *>::iterator it = listeners.begin(); it != listeners.end() ;)
    {
        (*it++)->onSyncFileStateChanged(api, sync, localPath, newState);
    }
}

#endif

void MegaApiImpl::fireOnBackupStateChanged(MegaScheduledCopyController *backup)
{
    assert(threadId == std::this_thread::get_id());
    for(set<MegaListener *>::iterator it = listeners.begin(); it != listeners.end() ;)
    {
        (*it++)->onBackupStateChanged(api, backup);
    }

    for(set<MegaScheduledCopyListener *>::iterator it = backupListeners.begin(); it != backupListeners.end() ;)
    {
        (*it++)->onBackupStateChanged(api, backup);
    }

    MegaScheduledCopyListener* listener = backup->getBackupListener();
    if(listener)
    {
        listener->onBackupStateChanged(api, backup);
    }
}


void MegaApiImpl::fireOnBackupStart(MegaScheduledCopyController *backup)
{
    for(set<MegaScheduledCopyListener *>::iterator it = backupListeners.begin(); it != backupListeners.end() ;)
    {
        (*it++)->onBackupStart(api, backup);
    }

    for(set<MegaListener *>::iterator it = listeners.begin(); it != listeners.end() ;)
    {
        (*it++)->onBackupStart(api, backup);
    }

    MegaScheduledCopyListener* listener = backup->getBackupListener();
    if(listener)
    {
        listener->onBackupStart(api, backup);
    }

}

void MegaApiImpl::fireOnBackupFinish(MegaScheduledCopyController *backup, unique_ptr<MegaErrorPrivate> e)
{
    for(set<MegaScheduledCopyListener *>::iterator it = backupListeners.begin(); it != backupListeners.end() ;)
    {
        (*it++)->onBackupFinish(api, backup, e.get());
    }

    for(set<MegaListener *>::iterator it = listeners.begin(); it != listeners.end() ;)
    {
        (*it++)->onBackupFinish(api, backup, e.get());
    }

    MegaScheduledCopyListener* listener = backup->getBackupListener();
    if(listener)
    {
        listener->onBackupFinish(api, backup, e.get());
    }
}

void MegaApiImpl::fireOnBackupTemporaryError(MegaScheduledCopyController *backup, unique_ptr<MegaErrorPrivate> e)
{
    for(set<MegaScheduledCopyListener *>::iterator it = backupListeners.begin(); it != backupListeners.end() ;)
    {
        (*it++)->onBackupTemporaryError(api, backup, e.get());
    }

    for(set<MegaListener *>::iterator it = listeners.begin(); it != listeners.end() ;)
    {
        (*it++)->onBackupTemporaryError(api, backup, e.get());
    }

    MegaScheduledCopyListener* listener = backup->getBackupListener();
    if(listener)
    {
        listener->onBackupTemporaryError(api, backup, e.get());
    }
}

void MegaApiImpl::fireOnBackupUpdate(MegaScheduledCopyController *backup)
{
    assert(threadId == std::this_thread::get_id());
//    notificationNumber++; //TODO: should we use notificationNumber for backups??

    for(set<MegaScheduledCopyListener *>::iterator it = backupListeners.begin(); it != backupListeners.end() ;)
    {
        (*it++)->onBackupUpdate(api, backup);
    }

    for(set<MegaListener *>::iterator it = listeners.begin(); it != listeners.end() ;)
    {
        (*it++)->onBackupUpdate(api, backup);
    }

    MegaScheduledCopyListener* listener = backup->getBackupListener();
    if(listener)
    {
        listener->onBackupUpdate(api, backup);
    }
}


#ifdef ENABLE_CHAT

void MegaApiImpl::fireOnChatsUpdate(MegaTextChatList *chats)
{
    assert(threadId == std::this_thread::get_id());
    for(set<MegaGlobalListener *>::iterator it = globalListeners.begin(); it != globalListeners.end() ;)
    {
        (*it++)->onChatsUpdate(api, chats);
    }
    for(set<MegaListener *>::iterator it = listeners.begin(); it != listeners.end() ;)
    {
        (*it++)->onChatsUpdate(api, chats);
    }
}

#endif

void MegaApiImpl::processTransferPrepare(Transfer *t, MegaTransferPrivate *transfer)
{
    transfer->setTotalBytes(t->size);
    transfer->setState(t->state);
    transfer->setPriority(t->priority);
    LOG_info << "Transfer (" << transfer->getTransferString() << ") starting. File: " << transfer->getFileName();
}

void MegaApiImpl::processTransferUpdate(Transfer *tr, MegaTransferPrivate *transfer)
{
    dstime currentTime = Waiter::ds;
    if (tr->slot)
    {
        m_off_t prevTransferredBytes = transfer->getTransferredBytes();
        m_off_t deltaSize = tr->slot->progressreported - prevTransferredBytes;
        LOG_verbose << "Transfer update: progress to update = " << deltaSize << ", transfer size = " << tr->size << ", transferred bytes = " << transfer->getTransferredBytes() << ", progress reported = " << tr->slot->progressreported << ", progress completed = " << tr->progresscompleted << " [speed: " << (tr->slot->speed / 1024) << " KB/s] [mean speed: " << (tr->slot->meanSpeed / 1024) << " KB/s] [transfer->name = " << tr->localfilename << "]";
        transfer->setStartTime(currentTime);
        transfer->setTransferredBytes(tr->slot->progressreported);
        transfer->setDeltaSize(deltaSize);
        transfer->setSpeed(tr->slot->speed);
        transfer->setMeanSpeed(tr->slot->meanSpeed);
    }
    else
    {
        LOG_verbose << "No TransferSlot. Reset last progress, speed and mean speed.";
        transfer->setDeltaSize(0);
        transfer->setSpeed(0);
        transfer->setMeanSpeed(0);
    }

    transfer->setState(tr->state);
    transfer->setPriority(tr->priority);
    transfer->setUpdateTime(currentTime);
    fireOnTransferUpdate(transfer);
}

void MegaApiImpl::processTransferComplete(Transfer *tr, MegaTransferPrivate *transfer)
{
    dstime currentTime = Waiter::ds;
    m_off_t deltaSize = tr->size - transfer->getTransferredBytes();
    LOG_verbose << "Transfer complete: final progress to update = " << deltaSize << ", transfer size = " << tr->size << ", transferred bytes = " << transfer->getTransferredBytes();
    transfer->setStartTime(currentTime);
    transfer->setUpdateTime(currentTime);
    transfer->setTransferredBytes(tr->size);
    transfer->setPriority(tr->priority);
    transfer->setDeltaSize(deltaSize);
    if (tr->slot)
    {
        transfer->setSpeed(tr->slot->speed);
        transfer->setMeanSpeed(tr->slot->meanSpeed);
    }

    if (tr->type == GET)
    {
        transfer->setState(MegaTransfer::STATE_COMPLETED);
        fireOnTransferFinish(transfer, std::make_unique<MegaErrorPrivate>(API_OK));
    }
    else
    {
        transfer->setState(MegaTransfer::STATE_COMPLETING);
        transfer->setTransfer(NULL);
        fireOnTransferUpdate(transfer);
    }
}

void MegaApiImpl::processTransferFailed(Transfer *tr, MegaTransferPrivate *transfer, const Error& e, dstime timeleft)
{
    auto megaError = std::make_unique<MegaErrorPrivate>(e, timeleft / 10);
    transfer->setStartTime(Waiter::ds);
    transfer->setUpdateTime(Waiter::ds);
    transfer->setDeltaSize(0);
    transfer->setSpeed(0);
    transfer->setMeanSpeed(0);
    transfer->setLastError(megaError.get());
    transfer->setPriority(tr->priority);
    if (e == API_ETOOMANY && e.hasExtraInfo())
    {
        transfer->setState(MegaTransfer::STATE_FAILED);
        transfer->setForeignOverquota(false);
        fireOnTransferFinish(transfer, std::move(megaError));
    }
    else
    {
        transfer->setState(MegaTransfer::STATE_RETRYING);
        LOG_verbose << "processTransferFailed checking handle " << transfer->getParentHandle();
        transfer->setForeignOverquota(e == API_EOVERQUOTA && client->isForeignNode(NodeHandle().set6byte(transfer->getParentHandle())));
        fireOnTransferTemporaryError(transfer, std::move(megaError));
    }

}

void MegaApiImpl::processTransferRemoved(Transfer *tr, MegaTransferPrivate *transfer, const Error& e)
{
    if (tr)
    {
        transfer->setPriority(tr->priority);
    }

    transfer->setStartTime(Waiter::ds);
    transfer->setUpdateTime(Waiter::ds);
    transfer->setState(e == API_EINCOMPLETE ? MegaTransfer::STATE_CANCELLED : MegaTransfer::STATE_FAILED);
    fireOnTransferFinish(transfer, std::make_unique<MegaErrorPrivate>(e));
}

MegaError *MegaApiImpl::checkAccessErrorExtended(MegaNode *megaNode, int level)
{
    if(!megaNode || level < MegaShare::ACCESS_UNKNOWN || level > MegaShare::ACCESS_OWNER)
    {
        return new MegaErrorPrivate(API_EARGS);
    }

    SdkMutexGuard g(sdkMutex);
    shared_ptr<Node> node = client->nodebyhandle(megaNode->getHandle());
    if(!node)
    {
        return new MegaErrorPrivate(API_ENOENT);
    }

    accesslevel_t a = OWNER;
    switch(level)
    {
        case MegaShare::ACCESS_UNKNOWN:
        case MegaShare::ACCESS_READ:
            a = RDONLY;
            break;
        case MegaShare::ACCESS_READWRITE:
            a = RDWR;
            break;
        case MegaShare::ACCESS_FULL:
            a = FULL;
            break;
        case MegaShare::ACCESS_OWNER:
            a = OWNER;
            break;
    }

    return client->checkaccess(node.get(), a) ? new MegaErrorPrivate(API_OK) : new MegaErrorPrivate(API_EACCESS);
}

MegaError *MegaApiImpl::checkMoveErrorExtended(MegaNode *megaNode, MegaNode *targetNode)
{
    if(!megaNode || !targetNode) return new MegaErrorPrivate(API_EARGS);

    SdkMutexGuard g(sdkMutex);
    shared_ptr<Node> node = client->nodebyhandle(megaNode->getHandle());
    shared_ptr<Node> target = client->nodebyhandle(targetNode->getHandle());
    if(!node || !target)
    {
        return new MegaErrorPrivate(API_ENOENT);
    }

    return new MegaErrorPrivate(client->checkmove(node.get(), target.get()));
}

bool MegaApiImpl::isFilesystemAvailable()
{
    SdkMutexGuard g(sdkMutex);
    return client->nodeByHandle(client->mNodeManager.getRootNodeFiles()) != NULL;
}

std::function<bool(Node*, Node*)> MegaApiImpl::getComparatorFunction(int order, MegaClient&)
{
    switch (order)
    {
        case MegaApi::ORDER_NONE: return nullptr;
        case MegaApi::ORDER_DEFAULT_ASC: return MegaApiImpl::nodeComparatorDefaultASC;
        case MegaApi::ORDER_DEFAULT_DESC: return MegaApiImpl::nodeComparatorDefaultDESC;
        case MegaApi::ORDER_SIZE_ASC: return MegaApiImpl::nodeComparatorSizeASC;
        case MegaApi::ORDER_SIZE_DESC: return MegaApiImpl::nodeComparatorSizeDESC;
        case MegaApi::ORDER_CREATION_ASC: return MegaApiImpl::nodeComparatorCreationASC;
        case MegaApi::ORDER_CREATION_DESC: return MegaApiImpl::nodeComparatorCreationDESC;
        case MegaApi::ORDER_MODIFICATION_ASC: return MegaApiImpl::nodeComparatorModificationASC;
        case MegaApi::ORDER_MODIFICATION_DESC:
            return MegaApiImpl::nodeComparatorModificationDESC;
        case MegaApi::ORDER_LINK_CREATION_ASC: return MegaApiImpl::nodeComparatorPublicLinkCreationASC;
        case MegaApi::ORDER_LINK_CREATION_DESC:
            return MegaApiImpl::nodeComparatorPublicLinkCreationDESC;
        case MegaApi::ORDER_LABEL_ASC: return MegaApiImpl::nodeComparatorLabelASC;
        case MegaApi::ORDER_LABEL_DESC: return MegaApiImpl::nodeComparatorLabelDESC;
        case MegaApi::ORDER_FAV_ASC: return MegaApiImpl::nodeComparatorFavASC;
        case MegaApi::ORDER_FAV_DESC: return MegaApiImpl::nodeComparatorFavDESC;
        case MegaApi::ORDER_SHARE_CREATION_ASC:
        case MegaApi::ORDER_SHARE_CREATION_DESC:
            return nullptr;
    }
    assert(false);
    return nullptr;
}

void MegaApiImpl::sortByComparatorFunction(sharedNode_vector&v, int order, MegaClient& mc)
{
    if (auto f = getComparatorFunction(order, mc))
    {
        std::sort(v.begin(), v.end(), [f](std::shared_ptr<Node> i, std::shared_ptr<Node> j) -> bool
        {
            return f(i.get(), j.get());
        });
    }
}

bool MegaApiImpl::nodeNaturalComparatorASC(Node *i, Node *j)
{
    int r = naturalsorting_compare(i->displayname(), j->displayname());
    if (r < 0)
    {
        return 1;
    }
    return 0;
}

bool MegaApiImpl::nodeNaturalComparatorDESC(Node *i, Node *j)
{
    int r = naturalsorting_compare(i->displayname(), j->displayname());
    if (r <= 0)
    {
        return 0;
    }
    return 1;
}

bool MegaApiImpl::nodeComparatorDefaultASC(Node *i, Node *j)
{
    int t = typeComparator(i, j);
    if (t >= 0)
    {
        return t != 0;
    }

    return nodeNaturalComparatorASC(i, j);
}

bool MegaApiImpl::nodeComparatorDefaultDESC(Node *i, Node *j)
{
    int t = typeComparator(i, j);
    if (t >= 0)
    {
        return t != 0;
    }

    return nodeNaturalComparatorDESC(i, j);
}

bool MegaApiImpl::nodeComparatorSizeASC(Node *i, Node *j)
{
    int t = typeComparator(i, j);
    if (t >= 0)
    {
        return t != 0;
    }

    m_off_t r = sizeDifference(i, j);
    if (r < 0)
    {
        return 1;
    }
    if (r > 0)
    {
        return 0;
    }
    return nodeNaturalComparatorASC(i, j);
}

bool MegaApiImpl::nodeComparatorSizeDESC(Node *i, Node *j)
{
    int t = typeComparator(i, j);
    if (t >= 0)
    {
        return t != 0;
    }

    m_off_t r = sizeDifference(i, j);
    if (r < 0)
    {
        return 0;
    }
    if (r > 0)
    {
        return 1;
    }
    return nodeNaturalComparatorDESC(i, j);
}

bool MegaApiImpl::nodeComparatorCreationASC(Node *i, Node *j)
{
    int t = typeComparator(i, j);
    if (t >= 0)
    {
        return t != 0;
    }
    if (i->ctime < j->ctime)
    {
        return 1;
    }
    if (i->ctime > j->ctime)
    {
        return 0;
    }
    return nodeNaturalComparatorASC(i, j);
}

bool MegaApiImpl::nodeComparatorCreationDESC(Node *i, Node *j)
{
    int t = typeComparator(i, j);
    if (t >= 0)
    {
        return t != 0;
    }
    if (i->ctime < j->ctime)
    {
        return 0;
    }
    if (i->ctime > j->ctime)
    {
        return 1;
    }
    return nodeNaturalComparatorDESC(i, j);
}

bool MegaApiImpl::nodeComparatorModificationASC(Node *i, Node *j)
{
    int t = typeComparator(i, j);
    if (t >= 0)
    {
        return t != 0;
    }

    if (i->type != FILENODE) // Only file nodes have last modified date
    {
        // If node doesn't have mtime, order alphabetically ascending
        return nodeNaturalComparatorASC(i, j);
    }

    m_time_t r = i->mtime - j->mtime;
    if (r < 0)
    {
        return 1;
    }
    if (r > 0)
    {
        return 0;
    }
    return nodeNaturalComparatorASC(i, j);
}

bool MegaApiImpl::nodeComparatorModificationDESC(Node *i, Node *j)
{
    int t = typeComparator(i, j);
    if (t >= 0)
    {
        return t != 0;
    }

    if (i->type != FILENODE)
    {
        // If node doesn't have mtime, order alphabetically descending
        return nodeNaturalComparatorDESC(i, j);
    }

    m_time_t r = i->mtime - j->mtime;
    if (r < 0)
    {
        return 0;
    }
    if (r > 0)
    {
        return 1;
    }

    return nodeNaturalComparatorDESC(i, j);
}

bool MegaApiImpl::nodeComparatorPublicLinkCreationASC(Node *i, Node *j)
{
    int t = typeComparator(i, j);
    if (t >= 0)
    {
        return t != 0;
    }
    if (!i->plink || !j->plink)
    {
        return nodeNaturalComparatorASC(i, j);
    }
    if (i->plink->cts < j->plink->cts)
    {
        return 1;
    }
    if (i->plink->cts > j->plink->cts)
    {
        return 0;
    }
    return nodeNaturalComparatorASC(i, j);
}

bool MegaApiImpl::nodeComparatorPublicLinkCreationDESC(Node *i, Node *j)
{
    int t = typeComparator(i, j);
    if (t >= 0)
    {
        return t != 0;
    }
    if (!i->plink || !j->plink)
    {
        return nodeNaturalComparatorDESC(i, j);
    }
    if (i->plink->cts < j->plink->cts)
    {
        return 0;
    }
    if (i->plink->cts > j->plink->cts)
    {
        return 1;
    }
    return nodeNaturalComparatorDESC(i, j);
}

bool MegaApiImpl::nodeComparatorLabelASC(Node *i, Node *j)
{
    nameid labelId = AttrMap::string2nameid("lbl");
    int iLabel = MegaNode::NODE_LBL_UNKNOWN;
    auto iAttrIt = i->attrs.map.find(labelId);
    if (iAttrIt != i->attrs.map.end())
    {
       iLabel = std::atoi(iAttrIt->second.c_str());
    }

    int jLabel = MegaNode::NODE_LBL_UNKNOWN;
    auto jAttrIt = j->attrs.map.find(labelId);
    if (jAttrIt != j->attrs.map.end())
    {
       jLabel = std::atoi(jAttrIt->second.c_str());
    }

    if (iLabel == MegaNode::NODE_LBL_UNKNOWN && jLabel ==  MegaNode::NODE_LBL_UNKNOWN)
    {
        return nodeComparatorDefaultASC(i, j);
    }
    if (iLabel == MegaNode::NODE_LBL_UNKNOWN)
    {
        return 0;
    }
    if (jLabel == MegaNode::NODE_LBL_UNKNOWN)
    {
        return 1;
    }

    if (iLabel < jLabel)
    {
        return 1;
    }
    if (iLabel > jLabel)
    {
        return 0;
    }

    int t = typeComparator(i, j);
    if (t >= 0)
    {
        return t != 0;
    }

    return nodeNaturalComparatorASC(i, j);
}

bool MegaApiImpl::nodeComparatorLabelDESC(Node* i, Node* j)
{
    nameid labelId = AttrMap::string2nameid("lbl");
    int iLabel = MegaNode::NODE_LBL_UNKNOWN;
    auto iAttrIt = i->attrs.map.find(labelId);
    if (iAttrIt != i->attrs.map.end())
    {
       iLabel = std::atoi(iAttrIt->second.c_str());
    }

    int jLabel = MegaNode::NODE_LBL_UNKNOWN;
    auto jAttrIt = j->attrs.map.find(labelId);
    if (jAttrIt != j->attrs.map.end())
    {
       jLabel = std::atoi(jAttrIt->second.c_str());
    }

    if (iLabel == MegaNode::NODE_LBL_UNKNOWN && jLabel == MegaNode::NODE_LBL_UNKNOWN)
    {
        return nodeComparatorDefaultASC(i, j);
    }
    if (iLabel == MegaNode::NODE_LBL_UNKNOWN)
    {
        return 0;
    }
    if (jLabel == MegaNode::NODE_LBL_UNKNOWN)
    {
        return 1;
    }

    if (iLabel < jLabel)
    {
        return 0;
    }
    if (iLabel > jLabel)
    {
        return 1;
    }

    int t = typeComparator(i, j);
    if (t >= 0)
    {
        return t != 0;
    }

    return nodeNaturalComparatorASC(i, j);
}

bool MegaApiImpl::nodeComparatorFavASC(Node *i, Node *j)
{
    nameid favId = AttrMap::string2nameid("fav");
    bool iFav = (i->attrs.map.find(favId) != i->attrs.map.end());
    bool jFav = (j->attrs.map.find(favId) != j->attrs.map.end());

    if (!(iFav ^ jFav))
    {
        // if both or none of them, have the same attribute value, order type and natural comparator
        // ASC
        int t = typeComparator(i, j);
        if (t >= 0)
        {
            return t != 0;
        }

        return nodeNaturalComparatorASC(i, j);
    }
    else if (iFav)
    {
        return 1;
    }
    else
    {
        return 0;
    }
}

bool MegaApiImpl::nodeComparatorFavDESC(Node *i, Node *j)
{
    nameid favId = AttrMap::string2nameid("fav");
    bool iFav = (i->attrs.map.find(favId) != i->attrs.map.end());
    bool jFav = (j->attrs.map.find(favId) != j->attrs.map.end());

    if (!(iFav ^ jFav))
    {
        // if both or none of them, have the same attribute value, order type and natural comparator
        // ASC
        int t = typeComparator(i, j);
        if (t >= 0)
        {
            return t != 0;
        }

        return nodeNaturalComparatorASC(i, j);
    }
    else if (iFav)
    {
        return 0;
    }
    else
    {
        return 1;
    }
}

// Compare node types. Returns -1 if i==j, 0 if i goes first, +1 if j goes first.
int MegaApiImpl::typeComparator(Node *i, Node *j)
{
    if (i->type < j->type)
    {
        return 0;
    }
    if (i->type > j->type)
    {
        return 1;
    }
    return -1;
}

int MegaApiImpl::getNumChildren(MegaNode* p)
{
    if (!p || p->getType() == MegaNode::TYPE_FILE)
    {
        return 0;
    }

    SdkMutexGuard lock(sdkMutex);
    return static_cast<int>(client->getNumberOfChildren(NodeHandle().set6byte(p->getHandle())));
}

int MegaApiImpl::getNumChildFiles(MegaNode* p)
{
    if (!p || p->getType() == MegaNode::TYPE_FILE)
    {
        return 0;
    }

    SdkMutexGuard lock(sdkMutex);
    std::shared_ptr<Node> parent = client->nodebyhandle(p->getHandle());
    if (!parent || parent->type == FILENODE)
    {
        return 0;
    }

    return static_cast<int>(client->mNodeManager.getNumberOfChildrenByType(parent->nodeHandle(), FILENODE));
}

int MegaApiImpl::getNumChildFolders(MegaNode* p)
{
    if (!p || p->getType() == MegaNode::TYPE_FILE)
    {
        return 0;
    }

    SdkMutexGuard lock(sdkMutex);
    std::shared_ptr<Node> parent = client->nodebyhandle(p->getHandle());
    if (!parent || parent->type == FILENODE)
    {
        return 0;
    }

    return static_cast<int>(client->mNodeManager.getNumberOfChildrenByType(parent->nodeHandle(), FOLDERNODE));
}

MegaNodeList *MegaApiImpl::getChildren(const MegaSearchFilter* filter, int order, CancelToken cancelToken, const MegaSearchPage* searchPage)
{
    // guard against unsupported or removed order criteria
    assert((MegaApi::ORDER_NONE <= order && order <= MegaApi::ORDER_MODIFICATION_DESC) ||
           (MegaApi::ORDER_LABEL_ASC <= order && order <= MegaApi::ORDER_FAV_DESC));

    // validations
    if (!filter || filter->byLocationHandle() == INVALID_HANDLE ||
        (filter->byNodeType() == MegaNode::TYPE_FOLDER && filter->byCategory() != MegaApi::FILE_TYPE_DEFAULT))
    {
        assert(filter && filter->byLocationHandle() != INVALID_HANDLE);
        return new MegaNodeListPrivate();
    }

    SdkMutexGuard guard(sdkMutex);

    NodeSearchFilter nf = searchToNodeFilter(*filter);

    const NodeSearchPage& np = searchPage ? NodeSearchPage(searchPage->startingOffset(), searchPage->size()) : NodeSearchPage(0u, 0u);
    sharedNode_vector results = client->mNodeManager.getChildren(nf, order, cancelToken, np);

    return new MegaNodeListPrivate(results);
}

MegaNodeList* MegaApiImpl::listChildNodesLexicographically(
    const handle parenthandle,
    CancelToken cancelFlag,
    const size_t maxElements,
    const std::optional<MegaSearchLexicographicalOffset>& offset)
{
    const auto megaToNodeOffset =
        [](const MegaSearchLexicographicalOffset& off) -> NodeSearchLexicographicalOffset
    {
        // Public off.mLastType is deprecated and ignored: node type is folded into the effective
        // S3 key, so the internal offset carries no type field at all.
        return {off.mLastName, off.mLastHandle};
    };
    SdkMutexGuard guard(sdkMutex);
    sharedNode_vector results =
        client->mNodeManager.listChildNodesLexicographically(parenthandle,
                                                             cancelFlag,
                                                             maxElements,
                                                             offset | transform(megaToNodeOffset));
    return new MegaNodeListPrivate(results);
}

MegaNodeList *MegaApiImpl::getChildren(const MegaNode* p, int order, CancelToken cancelToken)
{
    if (!p || p->getType() == MegaNode::TYPE_FILE)
    {
        return new MegaNodeListPrivate();
    }

    sharedNode_vector childrenNodes;

    SdkMutexGuard guard(sdkMutex);

    std::shared_ptr<Node> parent = client->nodebyhandle(p->getHandle());
    if (parent && parent->type != FILENODE)
    {
        sharedNode_list nodeList = client->getChildren(parent.get(), cancelToken);
        childrenNodes.reserve(nodeList.size());
        for (sharedNode_list::iterator it = nodeList.begin(); it != nodeList.end(); )
        {
            childrenNodes.push_back(*it++);
        }

        sortByComparatorFunction(childrenNodes, order, *client);
    }

    return new MegaNodeListPrivate(childrenNodes);
}

MegaNodeList *MegaApiImpl::getChildren(MegaNodeList *parentNodes, int order)
{
    SdkMutexGuard guard(sdkMutex);

    // prepare a vector with children of every parent node all together
    sharedNode_vector childrenNodes;
    for (int i = 0; i < parentNodes->size(); i++)
    {
        MegaNode *p = parentNodes->get(i);
        if (!p || p->getType() == MegaNode::TYPE_FILE)
        {
            continue;
        }

        std::shared_ptr<Node> parent = client->nodebyhandle(p->getHandle());
        if (parent && parent->type != FILENODE)
        {
            sharedNode_list nodeChildrenList = client->getChildren(parent.get());
            childrenNodes.reserve(childrenNodes.size() + nodeChildrenList.size());
            for (auto& node : nodeChildrenList)
            {
                childrenNodes.push_back(node);
            }
        }
    }

    sortByComparatorFunction(childrenNodes, order, *client);

    return new MegaNodeListPrivate(childrenNodes);
}

MegaNodeList *MegaApiImpl::getVersions(MegaNode *node)
{
    if (!node || node->getType() != MegaNode::TYPE_FILE)
    {
        return new MegaNodeListPrivate();
    }

    SdkMutexGuard g(sdkMutex);
    std::shared_ptr<Node> current = client->nodebyhandle(node->getHandle());
    if (!current || current->type != FILENODE)
    {
        return new MegaNodeListPrivate();
    }

    vector<std::shared_ptr<Node> > versions;
    versions.push_back(current);
    bool lookingFor = true;
    while (lookingFor)
    {
        sharedNode_list nodeList = client->getChildren(current.get());
        if (nodeList.empty())
        {
            lookingFor = false;
        }
        else
        {
            assert(nodeList.back()->parent == current);
            current = nodeList.back();
            assert(current->type == FILENODE);
            versions.push_back(current);
        }
    }

    return new MegaNodeListPrivate(versions);
}

int MegaApiImpl::getNumVersions(MegaNode *node)
{
    if (!node || node->getType() != MegaNode::TYPE_FILE)
    {
        return 0;
    }

    SdkMutexGuard guard(sdkMutex);
    return client->mNodeManager.getNumVersions(NodeHandle().set6byte(node->getHandle()));
}

bool MegaApiImpl::hasVersions(MegaNode *node)
{
    return getNumVersions(node) > 1;
}

bool MegaApiImpl::hasChildren(MegaNode *parent)
{
    if (!parent || parent->getType() == MegaNode::TYPE_FILE)
    {
        return false;
    }

    SdkMutexGuard g(sdkMutex);
    std::shared_ptr<Node> p = client->nodebyhandle(parent->getHandle());
    if (!p || p->type == FILENODE)
    {
        return false;
    }

    return !!client->getNumberOfChildren(p->nodeHandle());
}

MegaNode *MegaApiImpl::getChildNode(MegaNode *parent, const char* name)
{
    if (!parent || !name || parent->getType() == MegaNode::TYPE_FILE)
    {
        return NULL;
    }

    SdkMutexGuard guard(sdkMutex);
    std::shared_ptr<Node> parentNode = client->nodebyhandle(parent->getHandle());
    if (!parentNode || parentNode->type == FILENODE)
    {
        return NULL;
    }

    return MegaNodePrivate::fromNode(client->childnodebyname(parentNode.get(), name).get());
}

MegaNode* MegaApiImpl::getChildNodeOfType(MegaNode *parent, const char *name, int type)
{
    if (!name || !parent || (type != MegaNode::TYPE_FILE && type != MegaNode::TYPE_FOLDER))
    {
        return nullptr;
    }

    SdkMutexGuard guard(sdkMutex);
    std::shared_ptr<Node> parentNode = client->nodebyhandle(parent->getHandle());
    if (!parentNode || parentNode->type == FILENODE)
    {
        return nullptr;
    }

    return MegaNodePrivate::fromNode(client->childnodebynametype(parentNode.get(), name, static_cast<nodetype_t>(type)).get());
}

std::shared_ptr<Node> MegaApiImpl::getNodeByFingerprintInternal(const char *fingerprint)
{
    unique_ptr<FileFingerprint> fp(MegaApiImpl::getFileFingerprintInternal(fingerprint));
    if (!fp)
    {
        return NULL;
    }

    SdkMutexGuard g(sdkMutex);
    return client->mNodeManager.getNodeByFingerprint(*fp);
}

std::shared_ptr<Node> MegaApiImpl::getNodeByFingerprintInternal(const char *fingerprint, Node *parent)
{
    unique_ptr<FileFingerprint> fp(MegaApiImpl::getFileFingerprintInternal(fingerprint));
    if (!fp)
    {
        return NULL;
    }

    std::shared_ptr<Node> n = NULL;
    SdkMutexGuard g(sdkMutex);
    sharedNode_vector nodes = client->mNodeManager.getNodesByFingerprint(*fp);
    if (nodes.size())
    {
        n = nodes.at(0);
    }

    if (n && parent && n->parent.get() != parent)
    {
        for (unsigned int i = 1; i < nodes.size(); i++)
        {
            std::shared_ptr<Node> node = nodes.at(i);
            if (node->parent.get() == parent)
            {
                n = node;
                break;
            }
        }
    }
    return n;
}

FileFingerprint *MegaApiImpl::getFileFingerprintInternal(const char *fingerprint)
{
    m_off_t size = 0;
    string sfingerprint = MegaNodePrivate::removeAppPrefixFromFingerprint(fingerprint, &size);

    if (sfingerprint.empty()) return nullptr;

    FileFingerprint *fp = new FileFingerprint;
    if(!fp->unserializefingerprint(&sfingerprint))
    {
        delete fp;
        return NULL;
    }

    fp->size = size;

    return fp;
}

MegaNode* MegaApiImpl::getParentNode(MegaNode* n)
{
    if(!n) return NULL;

    SdkMutexGuard g(sdkMutex);
    std::shared_ptr<Node> node = client->nodebyhandle(n->getHandle());
    if(!node)
    {
        return NULL;
    }

    return MegaNodePrivate::fromNode(node->parent.get());
}

char* MegaApiImpl::getNodePath(MegaNode *node)
{
    if(!node) return nullptr;

    SdkMutexGuard guard(sdkMutex);
    std::shared_ptr<Node> n = client->nodebyhandle(node->getHandle());
    if(!n)
    {
        return nullptr;
    }
    return MegaApi::strdup(n->displaypath().c_str());
}

char* MegaApiImpl::getNodePathByNodeHandle(MegaHandle handle)
{
    SdkMutexGuard guard(sdkMutex);
    std::shared_ptr<Node> n = client->nodebyhandle(handle);
    if(!n)
    {
        return nullptr;
    }
    return MegaApi::strdup(n->displaypath().c_str());
}

MegaNode* MegaApiImpl::getNodeByPath(const char *path, MegaNode* node)
{
    SdkMutexGuard guard(sdkMutex);

    std::shared_ptr<Node> root = nullptr;

    if (node)
    {
        root = client->nodebyhandle(node->getHandle());
        if (!root)
        {
            return nullptr;
        }
    }

    std::shared_ptr<Node> result = client->nodeByPath(path, root);

    return MegaNodePrivate::fromNode(result.get());
}

MegaNode* MegaApiImpl::getNodeByPathOfType(const char* path, MegaNode* node, int type)
{
    SdkMutexGuard guard(sdkMutex);

    std::shared_ptr<Node> root = nullptr;

    if (node)
    {
        root = client->nodebyhandle(node->getHandle());
        if (!root)
        {
            return nullptr;
        }
    }

    nodetype_t t = (type == MegaNode::TYPE_FILE ? FILENODE :
                   (type == MegaNode::TYPE_FOLDER ? FOLDERNODE : TYPE_UNKNOWN));
    std::shared_ptr<Node> result = client->nodeByPath(path, root, t);

    return MegaNodePrivate::fromNode(result.get());
}

MegaNode* MegaApiImpl::getNodeByHandle(handle handle)
{
    if(handle == UNDEF) return NULL;
    SdkMutexGuard g(sdkMutex);
    return MegaNodePrivate::fromNode(client->nodebyhandle(handle).get());
}

MegaTotpTokenGenResult MegaApiImpl::generateTotpTokenFromNode(const MegaHandle handle)
{
    SdkMutexGuard g(sdkMutex);
    const auto result = client->generateTotpTokenFromNode(handle);
    return {result.first, {result.second.first, result.second.second}};
}

MegaContactRequest *MegaApiImpl::getContactRequestByHandle(MegaHandle handle)
{
    SdkMutexGuard guard(sdkMutex);
    auto it = client->pcrindex.find(handle);
    return it == client->pcrindex.end() ?
        nullptr :
        MegaContactRequestPrivate::fromContactRequest(it->second.get());
}

void MegaApiImpl::updateBackups()
{
    for (std::map<int, MegaScheduledCopyController *>::iterator it = backupsMap.begin(); it != backupsMap.end(); ++it)
    {
        MegaScheduledCopyController *backupController=it->second;
        backupController->update();
    }
}

bool MegaApiImpl::updateNodeMtime(std::shared_ptr<Node> node,
                                  MegaTransferPrivate* transfer,
                                  const m_time_t newMtime,
                                  const int nextTag)
{
    if (!transfer)
    {
        LOG_err << "updateNodeMtime: invalid transfer";
        assert(false);
        return false;
    }

    if (!node)
    {
        LOG_err << "updateNodeMtime: invalid node";
        assert(false);
        return false;
    }

    LOG_debug << "Updating mtime to node(" << toNodeHandle(node->nodeHandle()) << ")";
    const auto immediateErrCode = client->updateNodeMtime(
        node,
        newMtime,
        [this, nextTag](NodeHandle h, Error e)
        {
            MegaTransferPrivate* transfer = getMegaTransferPrivate(nextTag);
            if (!transfer)
            {
                LOG_debug << "updateNodeMtime for node(" << toNodeHandle(h)
                          << ") has finished with errorCode(" << e << "), but transfer with tag("
                          << nextTag << ") does not exists anymore";
                return;
            }

            if (e)
            {
                LOG_debug << "updateNodeMtime could not update mtime for node(" << toNodeHandle(h)
                          << "), errorCode(" << e << ")";
                transfer->setState(MegaTransfer::STATE_FAILED);
                fireOnTransferFinish(transfer, std::make_unique<MegaErrorPrivate>(API_EWRITE));
            }
            else
            {
                LOG_debug << "updateNodeMtime has finished successfully for node("
                          << toNodeHandle(h) << ")";

                transfer->setState(MegaTransfer::STATE_COMPLETED);
                fireOnTransferFinish(transfer, std::make_unique<MegaErrorPrivate>(e));
            }
        });

    if (immediateErrCode != API_OK)
    {
        LOG_debug << "updateNodeMtime immediate error for node(" << toNodeHandle(node->nodehandle)
                  << ")";
        return false;
    }

    transfer->setState(MegaTransfer::STATE_QUEUED);
    transferMap[nextTag] = transfer;
    transfer->setTag(nextTag);
    transfer->setTotalBytes(transfer->fingerprint_onDisk.size);
    transfer->setStartTime(Waiter::ds);
    transfer->setUpdateTime(Waiter::ds);
    fireOnTransferStart(transfer);
    transfer->setDeltaSize(transfer->fingerprint_onDisk.size);
    transfer->setSpeed(0);
    transfer->setMeanSpeed(0);
    transfer->setState(MegaTransfer::STATE_COMPLETING);
    fireOnTransferUpdate(transfer);
    return true;
}

MegaFilePut*
    MegaApiImpl::createMegaFileForRemoteCopyTransfer(MegaTransferPrivate& megaTransfer,
                                                     std::shared_ptr<Node> prevNodeSameName,
                                                     TransferDbCommitter& committer)
{
    auto inboxTarget = megaTransfer.getInboxTarget();

    string sname = megaTransfer.getFileName();
    bool isSourceTemporary = megaTransfer.isSourceFileTemporary();
    MegaFilePut* f = new MegaFilePut(client,
                                     megaTransfer.getLocalPath(),
                                     &sname,
                                     NodeHandle().set6byte(megaTransfer.getParentHandle()),
                                     inboxTarget.has_value() ? inboxTarget->c_str() : "",
                                     megaTransfer.getTime(),
                                     isSourceTemporary,
                                     prevNodeSameName);
    *static_cast<FileFingerprint*>(f) =
        megaTransfer.fingerprint_onDisk; // deliberate slicing - startxfer would re-fingerprint if
                                         // we don't supply this info
    f->setPitag(megaTransfer.getPitag());

    f->setTransfer(
        &megaTransfer); // sets internal `megaTransfer`, different from internal `transfer`!
    f->cancelToken = megaTransfer.accessCancelToken();

    Transfer* t = new Transfer(client, direction_t::PUT);
    t->skipserialization = false;
    *(FileFingerprint*)t = *(FileFingerprint*)f;
    t->tag = megaTransfer.getTag();
    f->tag = megaTransfer.getTag();

    t->size = megaTransfer.getTotalBytes();
    t->progresscompleted = megaTransfer.getTotalBytes();

    f->file_it = t->files.insert(t->files.end(), f);
    f->transfer = t;
    if (!f->dbid)
    {
        client->filecacheadd(f, committer);
    }

    f->prepare(*client->fsaccess);
    client->transferlist.addtransfer(t, committer, false);
    client->app->transfer_added(t);
    client->app->file_added(f);
    return f;
}

void MegaApiImpl::executeOnThread(shared_ptr<ExecuteOnce> f)
{
    MegaRequestPrivate* request = new MegaRequestPrivate(MegaRequest::TYPE_EXECUTE_ON_THREAD, nullptr);
    request->functionToExecute = std::move(f);
    requestQueue.push_front(request);  // these operations are part of requests that already queued, and should occur before other requests; queue at front
    waiter->notify();
}

bool CollisionChecker::CompareLocalFileMetaMac(FileAccess* fa, MegaNode* fileNode)
{
    if (fileNode->getNodeKey() == nullptr)
    {
        return false;
    }

    auto name =
        fileNode->getName() ? fileNode->getName() : std::optional<std::string>(std::nullopt);
    return CompareLocalFileMetaMacWithNodeKey(fa,
                                              *fileNode->getNodeKey(),
                                              fileNode->getType(),
                                              name)
        .areEqualMacs;
}

bool CollisionChecker::fingerprintEqualRelaxed(const FileFingerprint& lhs,
                                               const FileFingerprint& rhs)
{
#ifdef __ANDROID__
    return lhs.equalExceptMtime(rhs);
#else
    return lhs == rhs;
#endif
}

CollisionChecker::Result CollisionChecker::check(std::function<bool()> fingerprintEqualF, std::function<bool()> metamacEqualF, Option option)
{
    auto decision = CollisionChecker::Result::Download;

    switch (option)
    {
    case Option::AssumeSame:
    {
        decision = Result::Skip;
        break;
    }
    case Option::AlwaysError:
    {
        decision = Result::ReportError;
        break;
    }
    case Option::Fingerprint:
    {
        if (fingerprintEqualF())
        {
            decision = Result::Skip;
        }
        break;
    }
    case Option::Metamac:
    {
        if (metamacEqualF())
        {
            decision = Result::Skip;
        }
        break;
    }
    case Option::AssumeDifferent:
    {
        decision = Result::Download;
        break;
    }
    default:
        break;
    }

    return decision;

}

CollisionChecker::Result CollisionChecker::check(std::function<FileAccess*()> faGetter, MegaNode* fileNode, Option option)
{
    if (!fileNode)
    {
        return CollisionChecker::Result::NotYet;
    }

    auto fingerprintEqualF = [fileNode, faGetter]() {

        auto fa = faGetter();
        if (!fa)
        {
            return false;
        }

        unique_ptr<FileFingerprint> ff(MegaApiImpl::getFileFingerprintInternal(fileNode->getFingerprint()));
        if (!ff)
        {
            return false;
        }

        FileFingerprint fp;
        auto resGenFp = fp.genfingerprint(fa);
        return ff->isvalid && resGenFp && fp.isvalid && fingerprintEqualRelaxed(*ff, fp);
    };

    auto metaMacFunc = [fileNode, faGetter]() {

        auto fa = faGetter();
        if (!fa)
        {
            return false;
        }

        return CompareLocalFileMetaMac(fa, fileNode);
    };

    return check(
        fingerprintEqualF,
        metaMacFunc,
        option);
}

CollisionChecker::Result CollisionChecker::check(FileSystemAccess* fsaccess, const LocalPath& fileLocalPath, MegaNode* fileNode, Option option)
{
    auto fa = fsaccess->newfileaccess();
    auto fap = fa.get();
    auto faGetter = [fap, &fileLocalPath]()
    {
        return fap->fopen(fileLocalPath, OPEN_RDONLY, FSLogging::logExceptFileNotFound) &&
                       fap->type == FILENODE ?
                   fap :
                   nullptr;
    };
    return CollisionChecker::check(std::move(faGetter), fileNode, option);
}

CollisionChecker::Result CollisionChecker::check(std::function<FileAccess* ()> faGetter, Node* node, Option option)
{
    if (!node)
    {
        return CollisionChecker::Result::NotYet;
    }

    auto fingerprintEqualF = [node, faGetter]() {

        auto fa = faGetter();
        if (!fa)
        {
            return false;
        }

        FileFingerprint nodeFp = *node;
        FileFingerprint fp;
        return nodeFp.isvalid && fp.genfingerprint(fa) && fp.isvalid &&
               fingerprintEqualRelaxed(fp, nodeFp);
    };

    auto metaMacFunc = [node, faGetter]() {

        auto fa = faGetter();
        if (!fa)
        {
            return false;
        }

        return CompareLocalFileMetaMacWithNode(fa, node);
    };

    return check(
        fingerprintEqualF,
        metaMacFunc,
        option);
}

unsigned MegaApiImpl::sendPendingTransfers(TransferQueue *queue, MegaRecursiveOperation* recursiveTransfer, m_off_t availableDiskSpace)
{
    CodeCounter::ScopeTimer ccst(client->performanceStats.megaapiSendPendingTransfers);

    auto t0 = std::chrono::steady_clock::now();
    unsigned count = 0;

    SdkMutexGuard guard(sdkMutex);
    TransferDbCommitter committer(client->tctable);

    TransferQueue &auxQueue = queue
            ? *queue            // custom transferQueue used for folder uploads/downloads
            : transferQueue;    // transfer queue of class MegaApiImpl

    // if we are processing a folder transfer custom queue, we need to process in one shot
    // if the folder transfer cancel token is activated, each of the remaining
    // transfers will be straight away called back for start/finish rather than
    // passed to the SDK.
    bool canSplit = !queue;

    while (MegaTransferPrivate *transfer = auxQueue.pop())
    {
        error e = API_OK;
        int nextTag = client->nextreqtag();
        transfer->setState(MegaTransfer::STATE_QUEUED);

        if (transfer->accessCancelToken().isCancelled())
        {
            if (queue && recursiveTransfer && recursiveTransfer->isCancelledByFolderTransferToken())
            {
                // shortcut in case we have a huge queue
                // millions of listener callbacks, logging etc takes a while
                LOG_debug << "Folder transfer is cancelled, skipping remaining subtransfers: " << auxQueue.size();
                recursiveTransfer->setTransfersTotalCount(
                    recursiveTransfer->getTransfersTotalCount() - auxQueue.size());
                // Remove remaining transfers in recursiveTransfer's custom queue
                auxQueue.clear();
                // let the pop'd transfer notify the parent, we may be completely finished
                // otherwise the folder completes when transfers already established in the SDK core finish
            }

            transferMap[nextTag] = transfer;
            transfer->setTag(nextTag);
            transfer->setState(MegaTransfer::STATE_QUEUED);
            fireOnTransferStart(transfer);
            transfer->setStartTime(Waiter::ds);
            transfer->setUpdateTime(Waiter::ds);
            transfer->setState(MegaTransfer::STATE_CANCELLED);
            fireOnTransferFinish(transfer, std::make_unique<MegaErrorPrivate>(API_EINCOMPLETE));
            continue;
        }

        switch(transfer->getType())
        {
            case MegaTransfer::TYPE_UPLOAD:
            {
                LocalPath wLocalPath = transfer->getLocalPath();
                const char* fileName = transfer->getFileName();
                int64_t mtime = transfer->getTime();
                bool isSourceTemporary = transfer->isSourceFileTemporary();
                std::shared_ptr<Node> parent = client->nodebyhandle(transfer->getParentHandle());
                bool startFirst = transfer->shouldStartFirst();

                // This bool below is a bit tricky: for example, this would be true for
                // uploadForSupport: the MegaUploadOptionsPrivate::mTargetUser is populated with
                // MegaClient::SUPPORT_USER_HANDLE (length = 11), and that value is used to populate
                // transfer->parentPath (i.e.: it's not really a path, but a handle). At the same
                // time, parentHandle is undef. So "uploadToInbox" would be true here. Later, when
                // creating the MegaFilePut object, the cusertarget constructor param will have the
                // value of inboxTarget (see below), so MegaFilePut::targetuser will have the value
                // of MegaClient::SUPPORT_USER_HANDLE. This comparison (File::targetuser !=
                // MegaClient::SUPPORT_USER_HANDLE) can be used later to check if a transfer is for
                // support.
                auto inboxTarget = transfer->getInboxTarget();

                if (wLocalPath.empty() || !fileName || !(*fileName) ||
                    (!inboxTarget.has_value() && (!parent || parent->type == FILENODE)))
                {
                    e = API_EARGS;
                    break;
                }

                if (parent && parent->inshare && !client->checkaccess(parent.get(), RDWR))
                {
                    e = API_EACCESS;
                    break;
                }

                if (transfer->fingerprint_error != API_OK)
                {
                    LOG_debug << "Upload had already failed before queueing";
                    e = transfer->fingerprint_error;
                    break;
                }

                if (transfer->fingerprint_filetype == FILENODE && !transfer->fingerprint_onDisk.isvalid)
                {
                    LOG_debug << "Upload had already failed to be fingerprinted before queueing";
                    e = API_EREAD;
                    break;
                }

                assert(transfer->fingerprint_filetype == FILENODE ||
                       transfer->fingerprint_filetype == FOLDERNODE);

                if (transfer->fingerprint_filetype == FILENODE)
                {
                    FileFingerprint fp_forCloud = transfer->fingerprint_onDisk;
                    // don't clone an existing node unless it also already has the overridden mtime
                    // don't think that an existing file at this path is the right file unless it also has the overridden mtime
                    if (mtime != MegaApi::INVALID_CUSTOM_MOD_TIME)
                    {
                        fp_forCloud.mtime = mtime;
                    }

                    auto immediateFinishSameNodeNameFoundInTarget =
                        [nextTag, this](MegaTransferPrivate* transfer, const handle h)
                    {
                        if (!transfer)
                        {
                            LOG_err << "finishTransferSameNodeNameFoundInTarget: invalid transfer";
                            assert(false);
                            return;
                        }

                        LOG_debug << "Another node(" << toNodeHandle(h)
                                  << ") with same name, FP and MAC exists in target, and the "
                                  << "upload is not forced.";
                        transfer->setState(MegaTransfer::STATE_QUEUED);
                        transferMap[nextTag] = transfer;
                        transfer->setTag(nextTag);
                        transfer->setTotalBytes(transfer->fingerprint_onDisk.size);
                        transfer->setTransferredBytes(0);
                        transfer->setStartTime(Waiter::ds);
                        transfer->setUpdateTime(Waiter::ds);
                        fireOnTransferStart(transfer);

                        transfer->setNodeHandle(h);
                        transfer->setDeltaSize(transfer->fingerprint_onDisk.size);
                        transfer->setSpeed(0);
                        transfer->setMeanSpeed(0);
                        transfer->setState(MegaTransfer::STATE_COMPLETED);
                        fireOnTransferFinish(transfer, std::make_unique<MegaErrorPrivate>(API_OK));
                    };

                    auto forceToUpload{false};
                    const auto skipSearchBySameName =
                        !parent || client->getNumberOfChildren(parent->nodeHandle()) >
                                       MAX_CHILDREN_FOR_SAME_NAME_SEARCH;
                    const auto prevNodeSameName =
                        !skipSearchBySameName ?
                            client->childnodebyname(parent.get(), fileName, false) :
                            nullptr;

                    if (prevNodeSameName)
                    {
                        if (prevNodeSameName->type == FOLDERNODE)
                        {
                            if (!recursiveTransfer)
                            {
                                e = API_EARGS;
                                break;
                            }
                            /* In case we found a folder (in cloud drive) with a duplicate name for
                             * any subfile of the folder we are trying to upload SDK core will
                             * resolve the name conflict
                             *   - If versioning is enabled, it creates a new version.
                             *   - If versioning is disabled, it overwrites the file (old one is
                             * deleted permanently).
                             */
                        }
                        else if (forceToUpload =
                                     hasToForceUpload(*prevNodeSameName.get(), *transfer);
                                 !forceToUpload)
                        {
                            auto [compRes, _] = CompareLocalFileWithNodeMacAndFpExludingMtime(
                                *client,
                                wLocalPath,
                                fp_forCloud,
                                prevNodeSameName.get());

                            if (compRes == NODE_COMP_DIFFERS_MTIME)
                            {
                                if (auto succeeded = updateNodeMtime(prevNodeSameName,
                                                                     transfer,
                                                                     fp_forCloud.mtime,
                                                                     nextTag);
                                    succeeded)
                                {
                                    e = API_OK; // In case of mtime update, Transfer
                                                // (Start/Update/Finish) is delegated to
                                                // updateNodeMtime so ensure `e` is API OK to avoid
                                                // duplicated actions
                                    break;
                                }
                                // Fallback setmtime failed with immediate error, so let's try with
                                // clone node/regular upload
                            }
                            else if (compRes == NODE_COMP_EQUAL)
                            {
                                immediateFinishSameNodeNameFoundInTarget(
                                    transfer,
                                    prevNodeSameName->nodehandle);
                                break;
                            }
                        }
                        // Do not make transfer fail if CompareLocalFileWithNodeFpAndMac result
                        // is not NODE_COMP_EQUAL, just continue with transfer
                    }

                    // If has been found by name and it's necessary force upload, it isn't necessary
                    // look for it again
                    if (!forceToUpload)
                    {
                        std::shared_ptr<Node> sameNodeFpFound;
                        sharedNode_vector nodes =
                            client->mNodeManager.getNodesByFingerprint(fp_forCloud,
                                                                       true /*excludeMtime*/);

                        const auto alreadyCheckedSameNodeNameInTarget = !skipSearchBySameName;
                        bool sameNodeSameNameInTarget{false};

                        // MAC computation requires reading the local file (expensive I/O). When
                        // many cloud nodes share the same fingerprint, verifying all of them is
                        // wasteful. The two-phase search below minimizes MAC computations by
                        // prioritizing high-value candidates first and deferring the remote-copy
                        // fallback search.
                        const auto findNodeWithMacMatch = [&](auto shouldCheckMac) -> bool
                        {
                            for (auto& n: nodes)
                            {
                                if (!hasToForceUpload(*n.get(), *transfer))
                                {
                                    const bool nameMatch =
                                        (n->parent && parent) &&
                                        (n->parent->nodeHandle() == parent->nodeHandle()) &&
                                        (fileName == n->displayname());

                                    if (shouldCheckMac(nameMatch))
                                    {
                                        auto [compRes, _] =
                                            CompareLocalFileWithNodeMacAndFpExludingMtime(
                                                *client,
                                                wLocalPath,
                                                fp_forCloud,
                                                n.get());

                                        if (compRes == NODE_COMP_EQUAL ||
                                            compRes == NODE_COMP_DIFFERS_MTIME)
                                        {
                                            // There only can be one node with same name in target
                                            // node
                                            sameNodeFpFound = n;
                                            sameNodeSameNameInTarget = nameMatch;
                                            return true;
                                        }
                                    }
                                }
                            }
                            return false;
                        };

                        // Phase 1: check MAC only for nodes that are worth verifying immediately.
                        //   - Small folder (alreadyCheckedSameNodeNameInTarget == true): the name
                        //     lookup was already done, so any fingerprint-matching node is a valid
                        //     candidate; check all of them and break on the first MAC match.
                        //   - Large folder (alreadyCheckedSameNodeNameInTarget == false): the name
                        //     lookup was skipped; only check nodes whose name and parent match the
                        //     upload target, since those are the only ones that can skip the upload
                        //     entirely (same content, same location).
                        findNodeWithMacMatch(
                            [&](bool nameMatch)
                            {
                                return alreadyCheckedSameNodeNameInTarget || nameMatch;
                            });

                        // Phase 2: remote-copy fallback, only for large folders when Phase 1 found
                        // nothing. A cloud node at a different location but with the same content
                        // (MAC-verified) can be used as the source of a server-side copy, avoiding
                        // a full re-upload. nameMatch nodes are skipped here because they were
                        // already checked in Phase 1. Breaks at the first MAC-verified match, so
                        // typically only one extra MAC computation is needed.
                        if (!sameNodeFpFound && !alreadyCheckedSameNodeNameInTarget)
                        {
                            findNodeWithMacMatch(
                                [](bool nameMatch)
                                {
                                    return !nameMatch;
                                });
                        }

                        if (sameNodeFpFound)
                        {
                            const auto differentMtime{fp_forCloud.mtime != sameNodeFpFound->mtime};
                            if (sameNodeSameNameInTarget)
                            {
                                if (differentMtime)
                                {
                                    if (auto succeeded = updateNodeMtime(sameNodeFpFound,
                                                                         transfer,
                                                                         fp_forCloud.mtime,
                                                                         nextTag);
                                        succeeded)
                                    {
                                        e = API_OK; // In case of mtime update, Transfer
                                                    // (Start/Update/Finish) is delegated to
                                                    // updateNodeMtime so ensure `e` is API OK to
                                                    // avoid duplicated actions
                                        break;
                                    }
                                    // Fallback setmtime failed with immediate error, so let's try
                                    // with clone node
                                }
                                else
                                {
                                    immediateFinishSameNodeNameFoundInTarget(
                                        transfer,
                                        sameNodeFpFound->nodehandle);
                                    break;
                                }
                            }

                            LOG_debug << "Another node ("
                                      << toNodeHandle(sameNodeFpFound->nodehandle)
                                      << ") with same FP and MAC exists in Cloud. Perform remote "
                                         "copy";

                            Pitag pitag = transfer->getPitag();
                            pitag.purpose = PitagPurpose::CopyInternal;
                            pitag.nodeType = PitagNodeType::File;
                            if (pitag.target == PitagTarget::NotApplicable)
                            {
                                const bool inIncomingShare =
                                    parent && parent->matchesOrHasAncestorMatching(
                                                  [](const Node& node)
                                                  {
                                                      return node.inshare != nullptr;
                                                  });
                                pitag.target = inIncomingShare ? PitagTarget::IncomingShare :
                                                                 PitagTarget::CloudDrive;
                            }
                            transfer->setPitag(pitag);

                            currentTransfer = transfer;
                            transfer->setState(MegaTransfer::STATE_QUEUED);
                            transfer->setTag(nextTag);
                            transfer->setTotalBytes(transfer->fingerprint_onDisk.size);
                            transfer->setStartTime(Waiter::ds);
                            transfer->setUpdateTime(Waiter::ds);

                            auto f = createMegaFileForRemoteCopyTransfer(*transfer,
                                                                         sameNodeFpFound,
                                                                         committer);
                            e = client->transferRemoteCopy(
                                f,
                                sameNodeFpFound,
                                transfer->getFileName(),
                                parent,
                                nextTag,
                                differentMtime ? fp_forCloud :
                                                 std::optional<FileFingerprint>{std::nullopt},
                                inboxTarget);

                            if (e == API_OK)
                            {
                                transfer->setDeltaSize(transfer->fingerprint_onDisk.size);
                                transfer->setSpeed(0);
                                transfer->setMeanSpeed(0);
                            }
                            break;
                        }
                    }

                    currentTransfer = transfer;
                    string wFileName = fileName;
                    MegaFilePut* f =
                        new MegaFilePut(client,
                                        std::move(wLocalPath),
                                        &wFileName,
                                        NodeHandle().set6byte(transfer->getParentHandle()),
                                        inboxTarget.has_value() ? inboxTarget->c_str() : "",
                                        mtime,
                                        isSourceTemporary,
                                        prevNodeSameName);
                    *static_cast<FileFingerprint*>(f) = transfer->fingerprint_onDisk;  // deliberate slicing - startxfer would re-fingerprint if we don't supply this info

                    f->setTransfer(transfer); // sets internal `megaTransfer`, different from internal `transfer`!
                    f->cancelToken = transfer->accessCancelToken();
                    f->setPitag(transfer->getPitag());

                    error result = API_OK;
                    bool started = client->startxfer(PUT, f, committer, true, startFirst, transfer->isBackupTransfer(), UseLocalVersioningFlag, &result, nextTag);
                    if (!started)
                    {
                        transfer->setState(MegaTransfer::STATE_QUEUED);
                        if (!f->isvalid)
                        {
                            //Unable to read the file
                            transferMap[nextTag] = transfer;
                            transfer->setTag(nextTag);
                            fireOnTransferStart(transfer);
                            transfer->setStartTime(Waiter::ds);
                            transfer->setUpdateTime(Waiter::ds);
                            transfer->setState(MegaTransfer::STATE_FAILED);
                            fireOnTransferFinish(transfer, std::make_unique<MegaErrorPrivate>(API_EREAD));
                        }
                        else
                        {
                            MegaTransferPrivate* prevTransfer = NULL;
                            auto range = client->multi_transfers[PUT].equal_range(f);
                            for (auto it = range.first; it != range.second; ++it)
                            {
                                Transfer *t = it->second;
                                for (file_list::iterator fi = t->files.begin(); fi != t->files.end(); fi++)
                                {
                                    if (f->h != UNDEF && f->h == (*fi)->h && !f->targetuser.size()
                                            && !(*fi)->targetuser.size() && f->name == (*fi)->name)
                                    {
                                        prevTransfer = getMegaTransferPrivate((*fi)->tag);
                                        break;
                                    }
                                }
                            }

                            if (prevTransfer && transfer->getAppData())
                            {
                                string appData = prevTransfer->getAppData() ? string(prevTransfer->getAppData()) + "!" : string();
                                appData.append(transfer->getAppData());
                                prevTransfer->setAppData(appData.c_str());
                            }

                            //Already existing transfer
                            transferMap[nextTag] = transfer;
                            transfer->setTag(nextTag);
                            transfer->setTotalBytes(f->size);
                            fireOnTransferStart(transfer);
                            transfer->setStartTime(Waiter::ds);
                            transfer->setUpdateTime(Waiter::ds);
                            transfer->setState(MegaTransfer::STATE_CANCELLED);
                            fireOnTransferFinish(transfer, std::make_unique<MegaErrorPrivate>(result));
                        }

                        delete f; // `started` was false, `f` wasn't stored at Transfer::files
                    }
                    currentTransfer = NULL;
                }
                else
                {
                    transferMap[nextTag] = transfer;
                    transfer->setTag(nextTag);

                    transfer->startRecursiveOperation(std::make_shared<MegaFolderUploadController>(this, transfer), nullptr);
                }
                break;
            }
            case MegaTransfer::TYPE_DOWNLOAD:
            {
                std::shared_ptr<Node> node;
                MegaNode *publicNode = transfer->getPublicNode();
                MegaNode *nodeToUndelete = transfer->getNodeToUndelete();
                const char *parentPath = transfer->getParentPath();
                const char *fileName = transfer->getFileName();
                LocalPath path = transfer->getLocalPath();
                bool startFirst = transfer->shouldStartFirst();

                if (!publicNode)
                {
                    handle nodehandle = transfer->getNodeHandle();
                    node = nodeToUndelete ? nullptr : client->nodebyhandle(nodehandle);
                }

                if (!node && !nodeToUndelete && !publicNode)
                {
                    e = API_ENOENT;
                    break;
                }

                if (!transfer->isStreamingTransfer() && !parentPath && !fileName)
                {
                    e = API_EARGS;
                    break;
                }

                if (!transfer->isStreamingTransfer() && ((node && node->type != FILENODE) || (publicNode && publicNode->getType() != FILENODE)) )
                {
                    // Folder download
                    transferMap[nextTag] = transfer;
                    transfer->setTag(nextTag);

                    transfer->startRecursiveOperation(std::make_shared<MegaFolderDownloadController>(this, transfer), publicNode);
                    break;
                }

                // File download
                if (!transfer->isStreamingTransfer())
                {
                    LocalPath name;
                    LocalPath wLocalPath;

                    if (path.isURI())
                    {
                        wLocalPath = path.parentPath();
                    }
                    else if (parentPath)
                    {
                        wLocalPath = LocalPath::fromAbsolutePath(parentPath);
                    }
                    else
                    {
                        // Using the Absolute form, and passing "." means an absolute path of the current folder
                        // (ideally client apps would only pass absolute paths)
                        wLocalPath = LocalPath::fromAbsolutePath(".");
                        wLocalPath.appendWithSeparator(LocalPath::fromRelativePath(""), true);
                    }

                    FileSystemType fsType = transfer->getFileSystemType() != FileSystemType::FS_UNKNOWN
                                            ? transfer->getFileSystemType()
                                            : fsAccess->getlocalfstype(wLocalPath);

                    if (node)
                    {
                        if (!fileName)
                        {
                            attr_map::iterator ait = node->attrs.map.find('n');
                            if (ait == node->attrs.map.end())
                            {
                                name = LocalPath::fromRelativePath(Node::CRYPTO_ERROR);
                            }
                            else if(!ait->second.size())
                            {
                                name = LocalPath::fromRelativePath(Node::BLANK);
                            }
                            else
                            {
                                name =  LocalPath::fromRelativeName(ait->second, *fsAccess, fsType);
                            }
                        }
                        else
                        {
                            name = LocalPath::fromRelativeName(fileName, *fsAccess, fsType);
                        }
                    }
                    else
                    {
                        if (!transfer->getFileName())
                        {
                            name = LocalPath::fromRelativeName(publicNode->getName(), *fsAccess, fsType);
                        }
                        else
                        {
                            name = LocalPath::fromRelativeName(transfer->getFileName(), *fsAccess, fsType);
                        }
                    }
                    wLocalPath.appendWithSeparator(name, true);
                    transfer->setLocalPath(wLocalPath); // retry requires path set

                    // collision check if hasn't been checked yet
                    auto fa = fsAccess->newfileaccess();
                    if (fa &&
                        (transfer->getCollisionCheckResult() == CollisionChecker::Result::NotYet) &&
                        fa->fopen(wLocalPath, OPEN_RDONLY, FSLogging::logExceptFileNotFound) &&
                        fa->type == FILENODE)
                    {
                        auto fap = fa.get();
                        if (node)
                        {
                            transfer->setCollisionCheckResult(
                                CollisionChecker::check(
                                    [fap]() { return fap; }
                                    , node.get()
                                    , transfer->getCollisionCheck()));
                        }
                        else
                        {
                            transfer->setCollisionCheckResult(
                                CollisionChecker::check(
                                    [fap]() { return fap; }
                                    , publicNode
                                    , transfer->getCollisionCheck()));
                        }
                    }
                    else if (transfer->getCollisionCheckResult() == CollisionChecker::Result::NotYet) // no collision
                    {
                        transfer->setCollisionCheckResult(CollisionChecker::Result::Download);
                    }
                    // decision check for early returns
                    {
                        auto decision = transfer->getCollisionCheckResult();
                        if (decision == CollisionChecker::Result::ReportError)
                        {
                            e = API_EEXIST;
                            break;
                        }
                        else if (decision == CollisionChecker::Result::Skip) // complete with OK
                        {
                            CompleteFileDownloadBySkip(transfer,
                                node ? node->size : publicNode->getSize(),
                                node ? node->nodehandle : publicNode->getHandle(),
                                nextTag,
                                wLocalPath);
                            break;
                        }
                    }

                    currentTransfer = transfer;

                    unique_ptr<MegaFileGet> f;
                    if (node)
                    {
                        f.reset(new MegaFileGet(client, node.get(), wLocalPath, fsType, transfer->getCollisionResolution()));
                    }
                    else if (nodeToUndelete)
                    {
                        f.reset(new MegaFileGet(client, nodeToUndelete, wLocalPath, transfer->getCollisionResolution()));
                        f->setUndelete();
                    }
                    else
                    {
                        f.reset(new MegaFileGet(client, publicNode, wLocalPath, transfer->getCollisionResolution()));
                    }

                    f->setTransfer(transfer);
                    f->cancelToken = transfer->accessCancelToken();

                    bool skipDuplicates = transfer->getFolderTransferTag() <= 0; //Let folder subtransfer have duplicates, so that repeated downloads can co-exist and progress accordingly
                    mega::error cause;
                    bool ok = client->startxfer(GET, f.release(), committer, skipDuplicates, startFirst, false, UseLocalVersioningFlag, &cause, nextTag, availableDiskSpace);
                    if (!ok)
                    {
                        //Already existing transfer
                        transfer->setState(MegaTransfer::STATE_QUEUED);
                        transferMap[nextTag]=transfer;
                        transfer->setTag(nextTag);
                        fireOnTransferStart(transfer);

                        long long overquotaDelay = getBandwidthOverquotaDelay();
                        if (overquotaDelay)
                        {
                            fireOnTransferTemporaryError(transfer, std::make_unique<MegaErrorPrivate>(API_EOVERQUOTA, overquotaDelay));
                        }

                        transfer->setStartTime(Waiter::ds);
                        transfer->setUpdateTime(Waiter::ds);
                        transfer->setState(MegaTransfer::STATE_FAILED);
                        fireOnTransferFinish(transfer, std::make_unique<MegaErrorPrivate>(cause));
                    }
                }
                else
                {
                    currentTransfer = transfer;
                    m_off_t startPos = transfer->getStartPos();
                    m_off_t endPos = transfer->getEndPos();
                    if (startPos < 0 || endPos < 0 || startPos > endPos)
                    {
                        e = API_EARGS;
                        break;
                    }

                    if (node)
                    {
                        transfer->setFileName(node->displayname());
                        if (startPos >= node->size || endPos >= node->size)
                        {
                            e = API_EARGS;
                            break;
                        }

                        m_off_t totalBytes = endPos - startPos + 1;
                        transferMap[nextTag]=transfer;
                        transfer->setTotalBytes(totalBytes);
                        transfer->setTag(nextTag);
                        transfer->setState(MegaTransfer::STATE_QUEUED);

                        fireOnTransferStart(transfer);
                        client->pread(node.get(), startPos, totalBytes, transfer);
                        waiter->notify();
                    }
                    else
                    {
                        MegaNode* notOwnedNode = nodeToUndelete ? nodeToUndelete : publicNode;
                        transfer->setFileName(notOwnedNode->getName());
                        if (startPos >= notOwnedNode->getSize() || endPos >= notOwnedNode->getSize())
                        {
                            e = API_EARGS;
                            break;
                        }

                        // Refuse an invalid/unapplied key rather than
                        // build a zero-key cipher or read the IV past a short key buffer.
                        const std::string* nodeKey = notOwnedNode->getNodeKey();
                        if (!nodeKey || nodeKey->size() != FILENODEKEYLENGTH)
                        {
                            LOG_err << "Streaming download: foreign node "
                                    << toNodeHandle(notOwnedNode->getHandle())
                                    << " has an invalid key (size "
                                    << (nodeKey ? nodeKey->size() : 0) << ")";
                            e = API_EKEY;
                            break;
                        }

                        m_off_t totalBytes = endPos - startPos + 1;
                        transferMap[nextTag]=transfer;
                        transfer->setTotalBytes(totalBytes);
                        transfer->setTag(nextTag);
                        transfer->setState(MegaTransfer::STATE_QUEUED);
                        fireOnTransferStart(transfer);
                        SymmCipher cipher;
                        cipher.setkey(notOwnedNode->getNodeKey());

                        MegaNodePrivate* privateNode = dynamic_cast<MegaNodePrivate*>(notOwnedNode);
                        assert(privateNode);
                        client->pread(notOwnedNode->getHandle(),
                                      &cipher,
                                      MemAccess::get<int64_t>(
                                          (const char*)notOwnedNode->getNodeKey()->data() +
                                          SymmCipher::KEYLENGTH),
                                      startPos,
                                      totalBytes,
                                      transfer,
                                      !notOwnedNode->isForeign(),
                                      privateNode->getPrivateAuth()->c_str(),
                                      privateNode->getPublicAuth()->c_str(),
                                      privateNode->getChatAuth());
                        waiter->notify();
                    }
                }

                currentTransfer = NULL;
                break;
            }
        }

        if (e)
        {
            transferMap[nextTag] = transfer;
            transfer->setTag(nextTag);
            transfer->setState(MegaTransfer::STATE_QUEUED);
            fireOnTransferStart(transfer);
            transfer->setStartTime(Waiter::ds);
            transfer->setUpdateTime(Waiter::ds);
            transfer->setState(MegaTransfer::STATE_FAILED);
            fireOnTransferFinish(transfer, std::make_unique<MegaErrorPrivate>(e));
        }

        if (canSplit && (++count > 100 || std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::steady_clock::now() - t0).count() > 100))
        {
            break;
        }
    }
    return count;
}

void MegaApiImpl::CompleteFileDownloadBySkip(MegaTransferPrivate* transfer, m_off_t size, uint64_t nodehandle, int nextTag, const LocalPath& localPath)
{
    transfer->setState(MegaTransfer::STATE_QUEUED);
    transferMap[nextTag] = transfer;
    transfer->setTag(nextTag);
    transfer->setTotalBytes(size);
    transfer->setTransferredBytes(0);
    transfer->setLocalPath(localPath);
    transfer->setStartTime(Waiter::ds);
    transfer->setUpdateTime(Waiter::ds);
    fireOnTransferStart(transfer);
    transfer->setNodeHandle(nodehandle);
    transfer->setDeltaSize(size);
    transfer->setSpeed(0);
    transfer->setMeanSpeed(0);
    transfer->setState(MegaTransfer::STATE_COMPLETED);
    fireOnTransferFinish(transfer, std::make_unique<MegaErrorPrivate>(API_OK));
}

void MegaApiImpl::removeRecursively(const char *path)
{
#ifndef _WIN32
    auto localpath = LocalPath::fromPlatformEncodedAbsolute(path);
#ifndef __ANDROID__
    FSACCESS_CLASS::emptydirlocal(localpath);
#else
    if (auto fsa = createFSA(); dynamic_cast<AndroidFileSystemAccess*>(fsa.get()))
        AndroidFileSystemAccess::emptydirlocal(localpath);
    else
        LinuxFileSystemAccess::emptydirlocal(localpath);
#endif
#else
    auto localpath = LocalPath::fromAbsolutePath(path);
    WinFileSystemAccess::emptydirlocal(localpath);
#endif
}

error MegaApiImpl::processAbortBackupRequest(MegaRequestPrivate *request)
{
    int tag = int(request->getNumber());

    map<int, MegaScheduledCopyController *>::iterator itr = backupsMap.find(tag) ;
    if (itr != backupsMap.end())
    {
        MegaScheduledCopyController *backup = itr->second;

        bool flag = request->getFlag();
        if (!flag)
        {
            if (backup->getState() == MegaScheduledCopy::SCHEDULED_COPY_ONGOING)
            {
                for (std::map<int, MegaTransferPrivate *>::iterator it = transferMap.begin(); it != transferMap.end(); it++)
                {
                    MegaTransferPrivate *t = it->second;
                    if (t->getFolderTransferTag() == backup->getFolderTransferTag())
                    {
                        api->cancelTransferByTag(t->getTag()); //what if any of these fail? (Although I don't think that's possible)
                    }
                }
                request->setFlag(true);
                requestQueue.push(request);
            }
            else
            {
                LOG_debug << "Abort failed: no ongoing backup";
                fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(API_ENOENT));
            }
        }
        else
        {
            backup->abortCurrent(); //TODO: THIS MAY CAUSE NEW REQUESTS, should we consider them before fireOnRequestFinish?!!!
            fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(API_OK));
        }
        return API_OK;
    }
    else
    {
        return API_ENOENT;
    }
}

void MegaApiImpl::yield()
{
#if __cplusplus >= 201100L
    std::this_thread::yield();
#elif !defined(_WIN32)
    sched_yield();
#endif
}

static void appendFileAttribute(string& s, int n, MegaHandle h)
{
    if (h != INVALID_HANDLE)
    {
        if (!s.empty())
        {
            s += "/";
        }

        char buf[64];
        snprintf(buf, sizeof(buf), "%u*", n);
        Base64::btoa((byte*)&h, sizeof(h), strchr(buf + 2, 0));
        s += buf;
    }
}

static error updateAttributesMapWithCoordinates(attr_map& attrUpdates, int latitude, int longitude, bool unshareable, MegaClient* client)
{
    static const nameid coordsNameShareable = AttrMap::string2nameid("l");
    static const nameid coordsNameUnshareable = AttrMap::string2nameid("gp");

    if (longitude == MegaNode::INVALID_COORDINATE && latitude == MegaNode::INVALID_COORDINATE)
    {
        attrUpdates[coordsNameShareable] = "";
        attrUpdates[coordsNameUnshareable] = "";
    }
    else
    {
        if (longitude < 0 || longitude >= 0x01000000
            || latitude < 0 || latitude >= 0x01000000)
        {
            return API_EARGS;
        }

        Base64Str<3> latEncoded((const byte*)&latitude);
        Base64Str<3> lonEncoded((const byte*)&longitude);
        string coordsValue = string(latEncoded) + lonEncoded.chars;
        if (coordsValue.size() != 8)
        {
            return API_EARGS;
        }

        if (unshareable)
        {
            if (client->unshareablekey.size() != Base64Str<SymmCipher::KEYLENGTH>::STRLEN)
            {
                return API_EKEY;
            }

            SymmCipher c;
            byte data[SymmCipher::BLOCKSIZE] = { 0 };
            memcpy(data, "unshare/", 8);
            memcpy(data + 8, (void*)coordsValue.data(), coordsValue.size());
            client->setkey(&c, client->unshareablekey.data());
            c.ctr_crypt(data, unsigned(8 + coordsValue.size()), 0, 0, NULL, true);
            attrUpdates[coordsNameUnshareable] = Base64Str<SymmCipher::BLOCKSIZE>(data);
            attrUpdates[coordsNameShareable] = "";
        }
        else
        {
            attrUpdates[coordsNameShareable] = coordsValue;
            attrUpdates[coordsNameUnshareable] = "";
        }
    }
    return API_OK;
}

void MegaApiImpl::sendPendingScRequest()
{
    MegaRequestPrivate *request = scRequestQueue.front();
    if (!request || request->getTag())
    {
        return;
    }
    assert(request->getType() == MegaRequest::TYPE_CATCHUP);

    SdkMutexGuard g(sdkMutex);;

    request->setTag(1);
    fireOnRequestStart(request);
    client->catchup();
}

void MegaApiImpl::sendPendingRequests()
{
    SdkMutexGuard g(sdkMutex);

    // For multiple consecutive requests of the same type (eg. remove transfer) this committer will put all the database activity into a single commit
    TransferDbCommitter committer(client->tctable);
    int lastRequestType = -1;
    int lastRequestConsecutive = 0;

    error e = API_OK;
    MegaRequestPrivate *request = nullptr;
    bool abortBackoffTimer{true};
    constexpr dstime abortBackoffCoolDown{50}; // in deciseconds, 5 seconds

    while(1)
    {

        // Report the error from the last loop iteration (if any). (success must be reported directly by each case)
        // Having this code here is good for in-place conversions of case statements
        // to the `performRequest` system and reduce the switch size
        // And also shares this block of code between the switch and performRequest cases.
        if (e && request)
        {
            LOG_err << "Error starting request: " << e;
            fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e));
        }

        e = API_OK;

        request = requestQueue.pop();
        if (!request)
        {
            break;
        }

        // also we avoid yielding for consecutive transaction cancel operations (we used to yeild every time, but we need to keep the sdkMutex lock while the database transaction is ongoing)
        if ((lastRequestType == -1 || lastRequestType == request->getType()) && lastRequestConsecutive < 1024)
        {
            ++lastRequestConsecutive;
        }
        else
        {
            committer.commitNow();
            // Scoped retaining the request back into the requestQueue, allowing its listener
            // to be removed if removeListener is called in the scope by any chance
            const auto scopedRetaining = requestQueue.scopedRetainingRequest(request);
            g.unlock();
            yield();
            g.lock();
            lastRequestConsecutive = 0;
        }

        lastRequestType = request->getType();

        // Abort backoff timers if there are requests in the queue, but only once.
        // Do not abort backoff timers for logout or MegaRequest which will end in a lockless
        // request.
        if (abortBackoffTimer && request->getType() != MegaRequest::TYPE_LOGOUT &&
            !MegaRequestPrivate::causesLocklessRequest(request->getType()))
        {
            // Calculate the cool down before triggering a new abortbackoff
            if (Waiter::ds - latestAbortBackoffs > abortBackoffCoolDown)
            {
                client->abortbackoff(false);
                latestAbortBackoffs = Waiter::ds;
            }
            abortBackoffTimer = false;
        }

        if (request->getType() != MegaRequest::TYPE_EXECUTE_ON_THREAD)
        {
            if (!request->getTag())
            {
                int nextTag = client->nextreqtag();
                request->setTag(nextTag);
                requestMap[nextTag] = request;
                fireOnRequestStart(request);
            }
            else
            {
                // this case happens when we queue requests already started
            }
        }

        if (request->performRequest)
        {
            // the action should result in request destruction via fireOnRequestFinish
            // or a requeue of another step, etc.
            e = request->performRequest();
            continue;
        }

        if (request->performTransferRequest)
        {
            // the action has same requirement as the above performRequest
            e = request->performTransferRequest(committer);
            continue;
        }

        switch (request->getType())
        {
        default:
        {
            // Default case if not performRequest and not implemented below.
            // Keeping this at the top means we can convert the last case
            // to a performRequest while keeping the code in-place for diffs
            e = API_EINTERNAL;
            break;
        }
#ifdef ENABLE_SYNC
        case MegaRequest::TYPE_REMOVE_SYNCS:
        {
            assert(false); // this function deprecated, it wasn't used (and, questions about which error to report if multiple occur)
            e = API_EARGS;
            break;
        }
#endif
        case MegaRequest::TYPE_DELETE:
        {
#ifdef HAVE_LIBUV
            g.unlock();
            httpServerStop();
            ftpServerStop();
            g.lock();
#endif
            abortPendingActions();
            threadExit = 1;
            break;
        }
        case MegaRequest::TYPE_EXECUTE_ON_THREAD:
        {
            request->functionToExecute->exec();
            //requestMap.erase(request->getTag());  // per the test for TYPE_EXECUTE_ON_THREAD above, we didn't add it to the map or assign it a tag
            delete request;
            request = nullptr;
            break;
        }
        }
    }
}

void MegaApiImpl::putSet(MegaHandle sid, int optionFlags, const char* name, MegaHandle cover,
                         int type, MegaRequestListener* listener)
{
    MegaRequestPrivate* request = new MegaRequestPrivate(MegaRequest::TYPE_PUT_SET, listener);
    request->setParentHandle(sid);
    request->setParamType(optionFlags);
    request->setText(name);
    request->setNodeHandle(cover);
    request->setAccess(type);

    request->performRequest = [this, request]()
        {
            Set s;
            s.setId(request->getParentHandle());
            if (request->getParamType() & MegaApi::OPTION_SET_NAME)
            {
                s.setName(request->getText() ? request->getText() : string());
            }
            if (request->getParamType() & MegaApi::OPTION_SET_COVER)
            {
                s.setCover(request->getNodeHandle());
            }
            if (request->getParamType() & MegaApi::CREATE_SET)
            {
                const int t = request->getAccess();
                const int max = static_cast<int>(std::numeric_limits<uint8_t>::max());
                const int min = static_cast<int>(std::numeric_limits<uint8_t>::min());
                if (t > max || t < min)
                {
                    LOG_err << "Sets: type requested " << t << " is out of valid range [" << min << "," << max << "]";
                    fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(API_EARGS));
                    return API_OK;
                }
                s.setType(static_cast<Set::SetType>(t));
            }
            client->putSet(std::move(s),
                [this, request](Error e, const Set* s)
                {
                    if (request->getParentHandle() == UNDEF && s)
                    {
                        request->setMegaSet(std::make_unique<MegaSetPrivate>(*s));
                    }
                    fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e));
                });
            return API_OK;
        };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::removeSet(MegaHandle sid, MegaRequestListener* listener)
{
    MegaRequestPrivate* request = new MegaRequestPrivate(MegaRequest::TYPE_REMOVE_SET, listener);
    request->setParentHandle(sid);

    request->performRequest = [this, request]()
        {
            client->removeSet(request->getParentHandle(),
                [this, request](Error e)
                {
                    fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e));
                });
            return API_OK;
        };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::putSetElement(MegaHandle sid, MegaHandle eid, MegaHandle node, int optionFlags, int64_t order, const char* name, MegaRequestListener* listener)
{
    MegaRequestPrivate* request = new MegaRequestPrivate(MegaRequest::TYPE_PUT_SET_ELEMENT, listener);
    request->setTotalBytes(static_cast<long long>(sid));
    request->setParentHandle(eid);
    request->setNodeHandle(node);
    request->setParamType(optionFlags);
    request->setNumber(order);
    request->setText(name);

    request->performRequest = [this, request]()
        {
            SetElement el;
            el.setSet(static_cast<handle>(request->getTotalBytes()));
            el.setId(request->getParentHandle());
            el.setNode(request->getNodeHandle());
            if (request->getParamType() & MegaApi::OPTION_ELEMENT_ORDER)
            {
                el.setOrder(request->getNumber());
            }
            if (request->getParamType() & MegaApi::OPTION_ELEMENT_NAME)
            {
                el.setName(request->getText() ? request->getText() : string());
            }
            client->putSetElement(std::move(el),
                [this, request](Error e, const SetElement* el)
                {
                    if (e == API_OK) // only return SetElement upon create, not update
                    {
                        bool isNew = request->getParentHandle() == UNDEF;
                        assert(!isNew || el);
                        if (isNew && el)    // return the Element only when is created, not when updated
                        {
                            request->setMegaSetElementList(std::make_unique<MegaSetElementListPrivate>(&el, 1));
                        }
                    }
                    fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e));
                });
            return API_OK;
        };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::removeSetElement(MegaHandle sid, MegaHandle eid, MegaRequestListener* listener)
{
    MegaRequestPrivate* request = new MegaRequestPrivate(MegaRequest::TYPE_REMOVE_SET_ELEMENT, listener);
    request->setTotalBytes(static_cast<long long>(sid));
    request->setParentHandle(eid);

    request->performRequest = [this, request]()
    {
        client->removeSetElement(static_cast<handle>(request->getTotalBytes()),
                                 request->getParentHandle(),
                                 [this, request](Error e)
                                 {
                                     fireOnRequestFinish(request,
                                                         std::make_unique<MegaErrorPrivate>(e));
                                 });
        return API_OK;
    };

    requestQueue.push(request);
    waiter->notify();
}

error MegaApiImpl::performRequest_login(MegaRequestPrivate* request)
{
            const char *login = request->getEmail();
            const char *password = request->getPassword();
            const char* megaFolderLink = request->getLink();
            const char* sessionKey = request->getSessionKey();
            const bool tryToResumeFolderLinkFromCache{request->getFlag()};
            const auto cancelSnapshot{request->getTransferredBytes()};

            if (!megaFolderLink && (!(login && password)) && !sessionKey)
            {
                return API_EARGS;
            }

            string slogin;
            if (login)
            {
                slogin = login;
                slogin.erase(slogin.begin(), std::find_if(slogin.begin(), slogin.end(), char_is_not_space));
                slogin.erase(std::find_if(slogin.rbegin(), slogin.rend(), char_is_not_space).base(), slogin.end());
            }

            requestMap.erase(request->getTag());

            abortPendingActions();

            requestMap[request->getTag()]=request;

            error e = API_OK;
            client->locallogout(false, true);

            client->mLoginCancelSnapshot = static_cast<cancel_epoch_t>(cancelSnapshot);
            if (ScopedCanceller(client->mLoginCancelSnapshot).triggered())
            {
                // Should we abort at this point?
                LOG_warn << "[performRequest_login] A MegaApi locallogout triggered the cancel "
                            "epoch, which will affect this request";
            }

            if (sessionKey)
            {
                client->login(Base64::atob(string(sessionKey)));
            }
            else if (login && password && !megaFolderLink)
            {
                client->prelogin(slogin.c_str());
            }
            else
            {
                e = client->folderaccess(megaFolderLink, password, tryToResumeFolderLinkFromCache);
                if(e == API_OK)
                {
                    fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e));
                }
            }

            return e;
}

error MegaApiImpl::performRequest_tagNode(MegaRequestPrivate* request)
{
    std::shared_ptr<Node> node = client->nodebyhandle(request->getNodeHandle());
    int operation = request->getParamType();

    if (!node)
    {
        return API_EARGS;
    }

    if (!request->getText())
    {
        return API_EARGS;
    }

    std::string tag = request->getText();

    if (tag.find(MegaClient::TAG_DELIMITER) != std::string::npos)
    {
        return API_EARGS;
    }

    switch (operation)
    {
        case MegaApi::TAG_NODE_SET:
        {
            return client->addTagToNode(node,
                                        tag,
                                        [this, request](NodeHandle, Error e)
                                        {
                                            fireOnRequestFinish(request,
                                                                std::make_unique<MegaErrorPrivate>(e));
                                        });
        }
        case MegaApi::TAG_NODE_REMOVE:
        {
            return client->removeTagFromNode(
                node,
                tag,
                [this, request](NodeHandle, Error e)
                {
                    fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e));
                });
        }
        case MegaApi::TAG_NODE_UPDATE:
        {
            if (!request->getName())
            {
                return API_EARGS;
            }

            std::string oldTag = request->getName();

            return client->updateTagNode(
                node,
                tag,
                oldTag,
                [this, request](NodeHandle, Error e)
                {
                    fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e));
                });
        }
    }

    return API_EARGS;
}

void MegaApiImpl::CRUDNodeTagOperation(MegaNode* node,
                                       int operationType,
                                       const char* tag,
                                       const char* oldTag,
                                       MegaRequestListener* listener)
{
    MegaRequestPrivate* request = new MegaRequestPrivate(MegaRequest::TYPE_TAG_NODE, listener);

    if (node)
    {
        request->setNodeHandle(node->getHandle());
    }

    request->setParamType(operationType);
    request->setText(tag);
    request->setName(oldTag);

    request->performRequest = [this, request]()
    {
        return performRequest_tagNode(request);
    };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::multiFactorAuthCheck(const char* email, MegaRequestListener* listener)
{
    MegaRequestPrivate* request = new MegaRequestPrivate(MegaRequest::TYPE_MULTI_FACTOR_AUTH_CHECK, listener);
    request->setEmail(email);

    request->performRequest = [this, request]()
        {
            const char *email = request->getEmail();
            if (!email)
            {
                return API_EARGS;
            }
            client->multifactorauthcheck(email);
            return API_OK;
        };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::multiFactorAuthGetCode(MegaRequestListener* listener)
{
    MegaRequestPrivate* request = new MegaRequestPrivate(MegaRequest::TYPE_MULTI_FACTOR_AUTH_GET, listener);

    request->performRequest = [this]()
        {
            client->multifactorauthsetup();
            return API_OK;
        };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::multiFactorAuthEnableOrDisable(const char* pin, bool enable, MegaRequestListener* listener)
{
    MegaRequestPrivate* request = new MegaRequestPrivate(MegaRequest::TYPE_MULTI_FACTOR_AUTH_SET, listener);
    request->setFlag(enable);
    request->setPassword(pin);

    request->performRequest = [this, request]()
        {
            bool flag = request->getFlag();
            const char *pin = request->getPassword();
            if (!pin)
            {
                return API_EARGS;
            }

            if (flag)
            {
                client->multifactorauthsetup(pin);
            }
            else
            {
                client->multifactorauthdisable(pin);
            }
            return API_OK;
        };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::fetchTimeZone(bool forceApiFetch, MegaRequestListener* listener)
{
    MegaRequestPrivate* request = new MegaRequestPrivate(MegaRequest::TYPE_FETCH_TIMEZONE, listener);
    request->setFlag(forceApiFetch);

    request->performRequest = [this, request]()
        {
            if (!mTimezones || request->getFlag() /*forceApiFetch*/)
            {
                client->fetchtimezone();
            }
            else
            {
                request->setTimeZoneDetails(mTimezones);
                fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(API_OK));
            }
            return API_OK;
        };

    requestQueue.push(request);
    waiter->notify();
}

error MegaApiImpl::checkCreateFolderPrecons(const char* name,
                                            std::shared_ptr<Node> parent,
                                            MegaRequestPrivate* request)
{
    if (!name || !(*name) || !parent)
        return API_EARGS;

    // prevent to create a duplicate folder with same name in same path
    std::shared_ptr<Node> folder = client->childnodebyname(parent.get(), name, false);
    if (folder && folder->type == FOLDERNODE)
    {
        request->setNodeHandle(folder->nodehandle);
        return API_EEXIST;
    }

    return API_OK;
}

void MegaApiImpl::sendUserfeedback(const int rating,
                                   const char* comment,
                                   const bool transferFeedback,
                                   const int transferType)
{
    auto sendFeedback = [this](const int rating,
                               const std::string& base64comment,
                               const bool transferFeedback,
                               const direction_t transferType)
    {
        std::ostringstream feedback;
        if (transferFeedback)
        {
            std::string ts =
                client->mTransferStatsManager.metricsToJsonForTransferType(transferType);
            feedback << R"({\"r\":\")" << rating << R"(\",\"m\":\")" << base64comment
                     << R"(\",\"u\":\")" << toHandle(client->me) << R"(\",)" << ts << "}";
        }
        else
        {
            feedback << R"({\"r\":\")" << rating << R"(\",\"m\":\")" << base64comment
                     << R"(\",\"u\":\")" << toHandle(client->me) << R"(\"})";
        }
        client->userfeedbackstore(feedback.str().c_str());
    };

    std::string base64comment{};
    if (comment)
    {
        base64comment = Base64::btoa(comment);
    }

    if (transferFeedback)
    {
        if (transferType == MegaApi::TRANSFER_STATS_DOWNLOAD ||
            transferType == MegaApi::TRANSFER_STATS_BOTH)
        {
            sendFeedback(rating, base64comment, transferFeedback, GET);
        }

        if (transferType == MegaApi::TRANSFER_STATS_UPLOAD ||
            transferType == MegaApi::TRANSFER_STATS_BOTH)
        {
            sendFeedback(rating, base64comment, transferFeedback, PUT);
        }
    }
    else
    {
        sendFeedback(rating, base64comment, transferFeedback, NONE);
    }
}

void MegaApiImpl::createFolder(const char* name, MegaNode* parent, MegaRequestListener* listener)
{
    MegaRequestPrivate* request = new MegaRequestPrivate(MegaRequest::TYPE_CREATE_FOLDER, listener);
    if (parent) request->setParentHandle(parent->getHandle());
    request->setName(name);

    request->performRequest = [this, request]()
    {
        std::shared_ptr<Node> parent = client->nodebyhandle(request->getParentHandle());
        auto name = request->getName();
        if (const auto error = checkCreateFolderPrecons(name, parent, request); error != API_OK)
            return error;
        return client->createFolder(parent, name, request->getTag());
    };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::moveNode(MegaNode* node, MegaNode* newParent, const char* newName, MegaRequestListener* listener)
{
    MegaRequestPrivate* request = new MegaRequestPrivate(MegaRequest::TYPE_MOVE, listener);
    if (node) request->setNodeHandle(node->getHandle());
    if (newParent) request->setParentHandle(newParent->getHandle());
    request->setName(newName);

    request->performRequest = [this, request]()
        {
            std::shared_ptr<Node> node = client->nodebyhandle(request->getNodeHandle());
            std::shared_ptr<Node> newParent = client->nodebyhandle(request->getParentHandle());
            const char *name = request->getName();
            if (!node || !newParent)
            {
                return API_EARGS;
            }

            // Move to the current parent: compare parent handles, not node->parent.
            // parentHandle() comes from the node record and stays valid even when
            // node->parent is null; newParent from nodebyhandle() may be another
            // shared_ptr for the same folder (typical for in-shares).
            const bool sameParent = (node->parentHandle() == newParent->nodeHandle());

            if (sameParent)
            {
                const bool keepCurrentName = (name == nullptr || *name == '\0');
                bool sameName = keepCurrentName;
                if (!keepCurrentName)
                {
                    string normalized(name);
                    LocalPath::utf8_normalize(&normalized);
                    sameName = node->hasName(normalized);
                }

                if (sameName)
                {
                    fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(API_OK));
                    return API_OK;
                }
            }

            if (node->type == ROOTNODE || node->type == VAULTNODE || node->type == RUBBISHNODE ||
                !node->parent) // rootnodes cannot be moved
            {
                return API_EACCESS;
            }

            // old versions cannot be moved
            if (node->parent->type == FILENODE)
            {
                return API_EACCESS;
            }

            // target must be a folder with enough permissions
            if (newParent->type == FILENODE || !client->checkaccess(newParent.get(), RDWR))
            {
                return API_EACCESS;
            }

            error e = API_OK;
            e = client->checkmove(node.get(), newParent.get());
            if (e)
            {
                // If it's not possible to move the node, try copy-delete,
                // but only when it's not possible due to access rights
                // the node and the target are from different node trees,
                // it's possible to put nodes in the target folder
                // and also to remove the source node
                if (e != API_EACCESS)
                {
                    return e;
                }

                Node *nodeRoot = node.get();
                while (nodeRoot->parent)
                {
                    nodeRoot = nodeRoot->parent.get();
                }

                Node *parentRoot = newParent.get();
                while (parentRoot->parent)
                {
                    parentRoot = parentRoot->parent.get();
                }

                if ((nodeRoot == parentRoot)
                        || !client->checkaccess(node.get(), FULL)
                        || !client->checkaccess(newParent.get(), RDWR))
                {
                    return e;
                }

                TreeProcCopy tc;
                const bool fullInternalOperation = node && newParent && node->owner == client->me &&
                                                   client->me == newParent->owner;
                tc.resetSensitive = !fullInternalOperation;
                NodeHandle ovhandle;

                if (node->type == FILENODE)
                {
                    string newName;
                    if (name)
                    {
                        newName.assign(name);
                        LocalPath::utf8_normalize(&newName);
                    }
                    else
                    {
                        attr_map::iterator it = node->attrs.map.find('n');
                        if (it != node->attrs.map.end())
                        {
                            newName = it->second;
                        }
                    }
                    if (!newName.empty())
                    {
                        shared_ptr<Node> ovn =
                            client->childnodebyname(newParent.get(), newName.c_str(), true);
                        if (ovn)
                        {
                            if (ovn->nodeHandle() == node->nodeHandle())
                            {
                                fireOnRequestFinish(request,
                                                    std::make_unique<MegaErrorPrivate>(API_OK));
                                return API_OK;
                            }

                            if (node->isvalid && ovn->isvalid &&
                                *(FileFingerprint*)node.get() == *(FileFingerprint*)ovn.get())
                            {
                                request->setNodeHandle(UNDEF);
                                e = client->unlink(node.get(), false, request->getTag(), false);
                                return e;  // request finishes now if error, otherwise on unlink_result
                            }

                            ovhandle = ovn->nodeHandle();
                        }
                    }
                }

                // determine number of nodes to be copied
                client->proctree(node, &tc, !ovhandle.isUndef());
                if (tc.unusableKey)
                {
                    // A node in the tree has an unapplied key,
                    // fail the move rather than create a broken node.
                    LOG_err << "move: a node in the tree has an unusable key";
                    e = API_EKEY;
                    return e;
                }
                tc.allocnodes();

                client->proctree(node, &tc, !ovhandle.isUndef());
                if (tc.nn.empty())
                {
                    e = API_EARGS;
                    return e;
                }

                tc.nn[0].parenthandle = UNDEF;
                tc.nn[0].ovhandle = ovhandle;

                if (name)   // move and rename
                {
                    string newName(name);
                    LocalPath::utf8_normalize(&newName);

                    AttrMap attrs = node->attrs;
                    attrs.map['n'] = newName;

                    // We need to ensure we are not undoing the sensitive reset
                    if (tc.resetSensitive && attrs.map.erase(AttrMap::string2nameid("sen")))
                    {
                        LOG_debug << "Removing sen attribute";
                    }

                    string attrstring;
                    attrs.getjson(&attrstring);

                    SymmCipher key;
                    key.setkey((const byte*)tc.nn[0].nodekey.data(), node->type);
                    client->makeattr(&key, tc.nn[0].attrstring, attrstring.c_str());
                }

                // Mark node to be restored if moving to Rubbish Bin
                if (client->getrootnode(newParent)->type == RUBBISHNODE && client->getrootnode(node)->type != RUBBISHNODE)
                {
                    // "rr" attribute name and value
                    nameid rrname = AttrMap::string2nameid("rr");
                    Base64Str<MegaClient::NODEHANDLE> rrvalue(node->parent->nodehandle);
                    // Add attribute to a copy of old attributes
                    AttrMap attrs = node->attrs;
                    attrs.map[rrname] = rrvalue;
                    // Again, need to ensure we are not undoing the sensitive reset
                    if (tc.resetSensitive && attrs.map.erase(AttrMap::string2nameid("sen")))
                    {
                        LOG_debug << "Removing sen attribute when moving node to trash";
                    }

                    // Magic incantations for setting attributes
                    string attrstring;
                    attrs.getjson(&attrstring);

                    SymmCipher key;
                    key.setkey((const byte*)tc.nn[0].nodekey.data(), node->type);
                    client->makeattr(&key, tc.nn[0].attrstring, attrstring.c_str());
                }

                client->putnodes(newParent->nodeHandle(), UseLocalVersioningFlag, std::move(tc.nn), nullptr, request->getTag(), false);
                e = API_OK;
                return e;
            }

            e = client->rename(node, newParent, SYNCDEL_NONE, NodeHandle(), name, false,
                [request, this](NodeHandle h, Error e)
                {
                    request->setNodeHandle(h.as8byte());
                    fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e));
                });
            return e;
        };

    requestQueue.push(request);
    waiter->notify();
}

error MegaApiImpl::performRequest_copy(MegaRequestPrivate* request)
{
            std::shared_ptr<Node> node;
            std::shared_ptr<Node> target = client->nodebyhandle(request->getParentHandle());
            const char* email = request->getEmail();
            MegaNode *megaNode = request->getPublicNode();
            const char *newName = request->getName();
            NodeHandle ovhandle;

            if (!megaNode || (!target && !email)
                    || (newName && !(*newName))
                    || (target && target->type == FILENODE))
            {
                return API_EARGS;
            }

            if (!megaNode->isForeign() && !megaNode->isPublic())
            {
                node = client->nodebyhandle(request->getNodeHandle());
                if (!node)
                {
                    return API_ENOENT;
                }
            }

            const bool targetIsIncomingShare = target && target->matchesOrHasAncestorMatching(
                                                             [](const Node& n)
                                                             {
                                                                 return n.inshare != nullptr;
                                                             });
            Pitag pitag;
            pitag.target =
                targetIsIncomingShare ? PitagTarget::IncomingShare : PitagTarget::CloudDrive;

            if (!node)
            {
                if (!megaNode->getNodeKey()->size())
                {
                    return API_EKEY;
                }

                MegaNodePrivate* privateNode = dynamic_cast<MegaNodePrivate*>(megaNode);
                assert(privateNode);

                string sname = megaNode->getName();
                if (newName)
                {
                    if (privateNode)
                    {
                        sname = newName;
                        LocalPath::utf8_normalize(&sname);
                        privateNode->setName(sname.c_str());
                    }
                    else
                    {
                        LOG_err << "Unknown node type";
                    }
                }

                if (target && megaNode->getType() == MegaNode::TYPE_FILE)
                {
                    std::shared_ptr<Node> ovn = client->childnodebyname(target.get(), sname.c_str(), true);
                    if (ovn)
                    {
                        FileFingerprint *fp = getFileFingerprintInternal(megaNode->getFingerprint());
                        if (fp)
                        {
                            if (fp->isvalid && ovn->isvalid && *fp == *(FileFingerprint*)ovn.get())
                            {
                                request->setNodeHandle(ovn->nodehandle);
                                fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(API_OK));
                                delete fp;
                                return API_OK;
                            }

                            delete fp;
                        }

                        ovhandle = ovn->nodeHandle();
                    }
                }

                MegaTreeProcCopy tc(client);

                processMegaTree(megaNode, &tc);
                if (tc.unusableKey)
                {
                    LOG_err << "Failed to import node: a node in the tree has an unusable key";
                    return API_EKEY;
                }
                tc.allocnodes();

                // build new nodes array
                processMegaTree(megaNode, &tc);
                if (tc.nn.empty())
                {
                    LOG_err << "Failed to import node: no copyable nodes in the tree";
                    return API_EARGS;
                }

                tc.nn[0].parenthandle = UNDEF;
                tc.nn[0].ovhandle = ovhandle;

                pitag.purpose = PitagPurpose::Import;
                pitag.nodeType = megaNode->getType() == MegaNode::TYPE_FILE ? PitagNodeType::File :
                                                                              PitagNodeType::Folder;
                pitag.importSource = megaNode->getType() == MegaNode::TYPE_FILE ?
                                         PitagImportSource::FileLink :
                                         PitagImportSource::FolderLink;

                if (target)
                {
                    client->putnodes(target->nodeHandle(),
                                     UseLocalVersioningFlag,
                                     std::move(tc.nn),
                                     privateNode ? privateNode->getChatAuth() : nullptr,
                                     request->getTag(),
                                     false,
                                     {},
                                     nullptr,
                                     pitag);
                }
                else
                {
                    client->putnodes(email, std::move(tc.nn), request->getTag());
                }
            }
            else
            {
                pitag.purpose = PitagPurpose::Copy;
                pitag.nodeType =
                    node->type == FOLDERNODE ? PitagNodeType::Folder : PitagNodeType::File;
                pitag.importSource = node->inshare ? PitagImportSource::IncomingShare :
                                                     PitagImportSource::CloudDrive;

                vector<NewNode> nn;
                error err = copyTreeFromOwnedNode(node, newName, target, nn);
                if (err != API_OK)
                {
                    if (err == API_EEXIST) // dedicated error code when that exact same file already existed
                    {
                        assert(!nn.empty()); // never empty because other error should have been reported in that case
                        request->setNodeHandle(nn[0].ovhandle.as8byte());
                        fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(API_OK));
                        return API_OK;
                    }

                    return err;
                }

                assert(!nn.empty()); // never empty because an error should have been reported in that case
                nn[0].parenthandle = UNDEF;

                if (target)
                {
                    client->putnodes(target->nodeHandle(),
                                     UseLocalVersioningFlag,
                                     std::move(nn),
                                     nullptr,
                                     request->getTag(),
                                     false,
                                     {},
                                     nullptr,
                                     pitag);
                }
                else
                {
                    client->putnodes(email, std::move(nn), request->getTag());
                }
            }
            return API_OK;
}

error MegaApiImpl::copyTreeFromOwnedNode(shared_ptr<Node> node,
                                         const char* newName,
                                         shared_ptr<Node> target,
                                         vector<NewNode>& treeCopy,
                                         const std::optional<std::string>& s4AttributeValue)
{
    assert(node);
    assert(!newName || *newName);

    if (!node->nodekey().size())
    {
        LOG_err << "Failed to copy owned node: Node had no key";
        return API_EKEY;
    }

    if (node->attrstring)
    {
        node->applykey();
        node->setattr();
        if (node->attrstring)
        {
            LOG_err << "Failed to copy owned node: Node had bad key";
            return API_EKEY;
        }
    }

    // process new name
    string sname;
    if (newName)
    {
        sname = newName;
        LocalPath::utf8_normalize(&sname);
    }
    else
    {
        attr_map::iterator it = node->attrs.map.find('n');
        if (it != node->attrs.map.end())
        {
            sname = it->second;
        }
    }

    // determine handling of older versions
    NodeHandle ovhandle;
    bool fileAlreadyExisted = false;
    if (std::shared_ptr<Node> ovn = (node->type == FILENODE) ? client->getovnode(target.get(), &sname) : nullptr)
    {
        ovhandle = ovn->nodeHandle();
        fileAlreadyExisted = node->isvalid && ovn->isvalid && node->EqualExceptValidFlag(*ovn);
    }

    // determine number of nodes to be copied
    TreeProcCopy tc;
    const bool fullInternalOperation =
        node && target && node->owner == client->me && client->me == target->owner;
    tc.resetSensitive = !fullInternalOperation;
    client->proctree(node, &tc, false, !ovhandle.isUndef());
    if (tc.unusableKey)
    {
        LOG_err << "Failed to copy owned node: a node in the tree has an unusable key";
        return API_EKEY;
    }
    tc.allocnodes();

    // If the sensitivity was reset, the file didn't exist
    fileAlreadyExisted = fileAlreadyExisted && !(tc.resetSensitive && node->isMarkedSensitive());

    // build new nodes array
    client->proctree(node, &tc, false, !ovhandle.isUndef());
    if (tc.nn.empty())
    {
        LOG_err << "Failed to copy owned node: Failed to find nodes";
        return API_EARGS;
    }
    tc.nn[0].parenthandle = UNDEF;
    tc.nn[0].ovhandle = ovhandle;

    // Update name or S4 attr
    if (newName || s4AttributeValue)
    {
        SymmCipher key;
        AttrMap attrs;
        string attrstring;

        key.setkey((const byte*)tc.nn[0].nodekey.data(), node->type);
        attrs = node->attrs;

        if (newName)
        {
            attrs.map['n'] = sname;
        }

        if (s4AttributeValue)
        {
            const auto s4AttributeKey{AttrMap::string2nameid("s4")};
            if (const auto it{node->attrs.map.find(s4AttributeKey)};
                (it == node->attrs.map.end() && !s4AttributeValue->empty()) ||
                (it != node->attrs.map.end() && it->second != s4AttributeValue.value()))
            {
                if (fileAlreadyExisted)
                {
                    LOG_debug << "The s4 attribute has changed, so the file is no longer the same";

                    fileAlreadyExisted = false;
                }
                attrs.map[s4AttributeKey] = s4AttributeValue.value().c_str();
            }
        }

        // We need to ensure we are not undoing the sensitive reset
        if (tc.resetSensitive && attrs.map.erase(AttrMap::string2nameid("sen")))
        {
            LOG_debug << "Removing sen attribute";
        }

        attrs.getjson(&attrstring);
        client->makeattr(&key, tc.nn[0].attrstring, attrstring.c_str());
    }

    treeCopy = std::move(tc.nn);

    // If the exact same file was already there do not send the request.
    // Let the caller know by returning a dedicated error code, so that they
    // can continue as if API_OK was received from the cloud API.
    return fileAlreadyExisted ? API_EEXIST : API_OK;
}

void MegaApiImpl::restoreVersion(MegaNode* version, MegaRequestListener* listener)
{
    MegaRequestPrivate* request = new MegaRequestPrivate(MegaRequest::TYPE_RESTORE, listener);
    if (version)
    {
        request->setNodeHandle(version->getHandle());
    }

    request->performRequest = [this, request]()
        {
            std::shared_ptr<Node> version = client->nodebyhandle(request->getNodeHandle());
            if (!version)
            {
                return API_ENOENT;
            }

            if (version->type != FILENODE || !version->parent || version->parent->type != FILENODE)
            {
                return API_EARGS;
            }

            if (!version->keyApplied())
            {
                LOG_err << "restoreVersion: version " << toNodeHandle(version->nodehandle) << " ("
                        << version->displaypath() << ") has an unapplied key, cannot restore";
                return API_EKEY;
            }

            Node *current = version.get();
            while (current->parent && current->parent->type == FILENODE)
            {
                current = current->parent.get();
            }

            if (!current->parent)
            {
                return API_EINTERNAL;
            }

            vector<NewNode> newnodes(1);
            NewNode* newnode = &newnodes[0];
            string attrstring;
            SymmCipher key;

            newnode->source = NEW_NODE;
            newnode->type = FILENODE;
            newnode->nodehandle = version->nodehandle;
            newnode->parenthandle = UNDEF;
            newnode->ovhandle = current->nodeHandle();
            newnode->nodekey = version->nodekey();
            newnode->attrstring.reset(new string);
            if (newnode->nodekey.size())
            {
                key.setkey((const byte*)version->nodekey().data(), version->type);
                static constexpr std::array attributesToKeep{MegaClient::NODE_ATTRIBUTE_DESCRIPTION,
                                                             MegaClient::NODE_ATTRIBUTE_TAGS};
                std::for_each(std::begin(attributesToKeep),
                              std::end(attributesToKeep),
                              [&versionMap = version->attrs.map,
                               &currentMap = current->attrs.map](auto&& attribute)
                              {
                                  const auto attrId = AttrMap::string2nameid(attribute);
                                  versionMap[attrId] = currentMap[attrId];
                              });
                version->attrs.getjson(&attrstring);
                client->makeattr(&key, newnode->attrstring, attrstring.c_str());
            }

            client->putnodes(current->parent->nodeHandle(), ClaimOldVersion, std::move(newnodes), nullptr, request->getTag(), false);
            return API_OK;
        };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::renameNode(MegaNode* node, const char* newName, MegaRequestListener* listener)
{
    MegaRequestPrivate* request = new MegaRequestPrivate(MegaRequest::TYPE_RENAME, listener);
    if (node) request->setNodeHandle(node->getHandle());
    request->setName(newName);

    request->performRequest = [this, request]() -> error
    {
        const auto nh = NodeHandle{}.set6byte(request->getNodeHandle());
        CommandSetAttr::Completion cb = [request, this](NodeHandle h, Error e)
        {
            assert(request->getNodeHandle() == h.as8byte());
            request->setNodeHandle(h.as8byte());
            fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e));
        };

        return client->renameNode(nh, request->getName(), std::move(cb));
    };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::remove(MegaNode* node, bool keepversions, MegaRequestListener* listener)
{
    MegaRequestPrivate* request = new MegaRequestPrivate(MegaRequest::TYPE_REMOVE, listener);
    if (node) request->setNodeHandle(node->getHandle());
    request->setFlag(keepversions);

    request->performRequest = [this, request]() -> error
    {
        const auto nh = NodeHandle{}.set6byte(request->getNodeHandle());
        return client->removeNode(nh, request->getFlag(), request->getTag());
    };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::removeVersions(MegaRequestListener* listener)
{
    MegaRequestPrivate* request = new MegaRequestPrivate(MegaRequest::TYPE_REMOVE_VERSIONS, listener);

    request->performRequest = [this]()
        {
            client->unlinkversions();
            return API_OK;
        };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::share(MegaNode* node, const char* email, int access, MegaRequestListener* listener)
{
    MegaRequestPrivate* request = new MegaRequestPrivate(MegaRequest::TYPE_SHARE, listener);
    if (node) request->setNodeHandle(node->getHandle());
    request->setEmail(email);
    request->setAccess(access);

    request->performRequest = [this, request]()
        {
            std::shared_ptr<Node> node = client->nodebyhandle(request->getNodeHandle());
            const char* email = request->getEmail();
            int access = request->getAccess();
            if(!node || !email || !strchr(email, '@'))
            {
                return API_EARGS;
            }
            error e = API_OK;
            accesslevel_t a = ACCESS_UNKNOWN;
            switch(access)
            {
                case MegaShare::ACCESS_UNKNOWN:
                    a = ACCESS_UNKNOWN;
                    break;
                case MegaShare::ACCESS_READ:
                    a = RDONLY;
                    break;
                case MegaShare::ACCESS_READWRITE:
                    a = RDWR;
                    break;
                case MegaShare::ACCESS_FULL:
                    a = FULL;
                    break;
                case MegaShare::ACCESS_OWNER:
                    a = OWNER;
                    break;
                default:
                    e = API_EARGS;
            }

            if (e == API_OK)
            {
                client->setshare(node, email, a, false, nullptr, request->getTag(), [this, request](Error e, bool){
                    fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e));
                });
            }
            return e;
        };

    requestQueue.push(request);
    waiter->notify();
}

error MegaApiImpl::performRequest_importLink_getPublicNode(MegaRequestPrivate* request)
{
            std::shared_ptr<Node> node = client->nodebyhandle(request->getParentHandle());
            const char* megaFileLink = request->getLink();

            if (!megaFileLink)
            {
                return API_EARGS;
            }
            if ((request->getType() == MegaRequest::TYPE_IMPORT_LINK) && !node)
            {
                return API_EARGS;
            }

            handle ph = UNDEF;
            byte key[FILENODEKEYLENGTH];
            error e = client->parsepubliclink(megaFileLink, ph, key, TypeOfLink::FILE);
            if (e == API_OK)
            {
                client->openfilelink(ph, key);
            }
            else if (e == API_EINCOMPLETE)  // no key provided, check only the existence of the node
            {
                client->openfilelink(ph, nullptr);
                e = API_OK;
            }
            return e;
}

void MegaApiImpl::getDownloadUrl(MegaNode* node,
                                 bool singleUrl,
                                 bool forceSSL,
                                 MegaRequestListener* listener)
{
    MegaRequestPrivate* request = new MegaRequestPrivate(MegaRequest::TYPE_GET_DOWNLOAD_URLS, listener);
    request->setFlag(singleUrl);
    request->setAccess(forceSSL ? 1 : 0);
    if (node) request->setNodeHandle(node->getHandle());

    request->performRequest = [this, request]()
        {
            std::shared_ptr<Node> node = client->nodebyhandle(request->getNodeHandle());
            if(!node) // works only for existing nodes, not the ones that need to be undeleted (see "gd" command)
            {
                return API_EARGS;
            }

            // Refuse nodes whose key has not been applied. Such a node still holds
            // the raw wire-form key ("<sharehandle>:<base64>"), longer than the
            // fixed filekey buffer in CommandGetFile.
            if (!node->keyApplied())
            {
                return API_EKEY;
            }

            client->queueCommand(new CommandGetFile(
                client,
                (const byte*)node->nodekey().data(),
                node->nodekey().size(),
                false /*undelete*/,
                node->nodehandle,
                true,
                nullptr,
                nullptr,
                nullptr,
                request->getFlag() /*singleUrl*/,
                static_cast<bool>(request->getAccess()) /*forceSSL*/,
                [this, request, h = node->nodehandle](const Error& e,
                                                      m_off_t /*size*/,
                                                      dstime /*timeleft*/,
                                                      std::string* /*filename*/,
                                                      std::string* /*fingerprint*/,
                                                      std::string* /*fileattrstring*/,
                                                      const std::vector<std::string>& urls,
                                                      const std::vector<std::string>& ips,
                                                      const std::string& fileHandle)
                {
                    if (e == API_OK && urls.size() && ips.size())
                    {
                        string surls, sipsv4, sipsv6;

                        auto delimFields=";";
                        for (auto &u : urls)
                        {
                            if (!surls.empty())
                                surls.append(delimFields);
                            surls.append(u);
                        }
                        bool ipv4 = true;
                        for (auto &ip : ips)
                        {
                            auto &w = ipv4 ? sipsv4 : sipsv6;
                            if (!w.empty())
                                w.append(delimFields);
                            w.append(ip);
                            ipv4 = !ipv4;
                        }

                        request->setName(surls.c_str());
                        request->setLink(sipsv4.c_str());
                        request->setText(sipsv6.c_str());
                        request->setMegaStringMap({
                            {Base64Str<MegaClient::NODEHANDLE>(h).chars, fileHandle}
                        });
                    }

                    fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e));
                    return true;
                }));

            return API_OK;
        };

    requestQueue.push(request);
    waiter->notify();
}

error MegaApiImpl::performRequest_passwordLink(MegaRequestPrivate* request)
{
            const char *link = request->getLink();
            const char *pwd = request->getPassword();
            bool encryptLink = request->getFlag();

            error e = API_OK;
            string result;
            if (encryptLink)
            {
                e = client->encryptlink(link, pwd, &result);
            }
            else
            {
                e = client->decryptlink(link, pwd, &result);
            }

            if (!e)
            {
                request->setText(result.c_str());
                fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e));
            }
            return e;
}

error MegaApiImpl::performRequest_export(MegaRequestPrivate* request)
{
            std::shared_ptr<Node> node = client->nodebyhandle(request->getNodeHandle());
            if (!node) { return API_EARGS; }

            bool writable = request->getFlag();
            bool megaHosted = request->getTransferTag() != 0;
            // exportnode() will take care of creating a share first, should it be a folder
            return client->exportnode(
                node,
                !request->getAccess(),
                request->getNumber(),
                writable,
                megaHosted,
                request->getTag(),
                [this, request, megaHosted](Error e, handle h, handle ph, string&& encryptionKey)
                {
                    if (e || !request->getAccess()) // disable export doesn't return h and ph
                    {
                        fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e));
                    }
                    else
                    {
                        // This block of code used to be inside exportnode_result
                        if (std::shared_ptr<Node> n = client->nodebyhandle(h))
                        {
                            char key[FILENODEKEYLENGTH * 4 / 3 + 3];

                            // the key
                            if (n->type == FILENODE)
                            {
                                if (n->nodekey().size() >= FILENODEKEYLENGTH)
                                {
                                    Base64::btoa((const byte*)n->nodekey().data(),
                                                 FILENODEKEYLENGTH,
                                                 key);
                                }
                                else
                                {
                                    key[0] = 0;
                                }
                            }
                            else if (n->sharekey)
                            {
                                Base64::btoa(n->sharekey->key, FOLDERNODEKEYLENGTH, key);
                            }
                            else
                            {
                                fireOnRequestFinish(
                                    request,
                                    std::make_unique<MegaErrorPrivate>(MegaError::API_EKEY));
                                return;
                            }

                            TypeOfLink lType = client->validTypeForPublicURL(n->type);
                            string link =
                                client->publicLinkURL(client->mNewLinkFormat, lType, ph, key);
                            request->setLink(link.c_str());
                            if (n->plink && n->plink->mAuthKey.size())
                            {
                                request->setPrivateKey(n->plink->mAuthKey.c_str());
                                if (megaHosted)
                                {
                                    request->setPassword(encryptionKey.c_str());
                                }
                            }

                            fireOnRequestFinish(
                                request,
                                std::make_unique<MegaErrorPrivate>(MegaError::API_OK));
                        }
                        else
                        {
                            request->setNodeHandle(UNDEF);
                            fireOnRequestFinish(
                                request,
                                std::make_unique<MegaErrorPrivate>(MegaError::API_ENOENT));
                        }
                    }
                });
}

void MegaApiImpl::fetchNodes(MegaRequestListener* listener)
{
    MegaRequestPrivate* request = new MegaRequestPrivate(MegaRequest::TYPE_FETCH_NODES, listener);

    request->performRequest = [this]()
        {
            bool forceLoadFromServers = nocache;
            nocache = false;

            client->fetchnodes(client->fetchnodesAlreadyCompletedThisSession,
                              !client->syncsAlreadyLoadedOnStatecurrent,
                               forceLoadFromServers);

            return API_OK;
        };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::getCloudStorageUsed(MegaRequestListener* listener)
{
    MegaRequestPrivate* request = new MegaRequestPrivate(MegaRequest::TYPE_GET_CLOUD_STORAGE_USED, listener);

    request->performRequest = [this, request]()
        {
            if (client->loggedin() != FULLACCOUNT && !client->loggedIntoFolder())
            {
                return API_EACCESS;
            }

            NodeCounter nc = client->mNodeManager.getCounterOfRootNodes();
            request->setNumber(nc.storage + nc.versionStorage);
            fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(API_OK));
            return API_OK;
        };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::getAccountDetails(bool storage, bool transfer, bool pro, bool sessions, bool purchases, bool transactions, int source, MegaRequestListener* listener)
{
    MegaRequestPrivate* request = new MegaRequestPrivate(MegaRequest::TYPE_ACCOUNT_DETAILS, listener);
    int numDetails = 0;
    if (storage) numDetails |= 0x01;
    if (transfer) numDetails |= 0x02;
    if (pro) numDetails |= 0x04;
    if (transactions) numDetails |= 0x08;
    if (purchases) numDetails |= 0x10;
    if (sessions) numDetails |= 0x20;
    request->setNumDetails(numDetails);
    request->setAccess(source);

    request->performRequest = [this, request]()
    {
        if (client->loggedin() == NOTLOGGEDIN)
        {
            return API_EACCESS;
        }

        int numDetails = request->getNumDetails();
        bool storage = (numDetails & 0x01) != 0;
        bool transfer = (numDetails & 0x02) != 0;
        bool pro = (numDetails & 0x04) != 0;
        bool transactions = (numDetails & 0x08) != 0;
        bool purchases = (numDetails & 0x10) != 0;
        bool sessions = (numDetails & 0x20) != 0;

        int numReqs =
            int(storage || transfer || pro) + int(transactions) + int(purchases) + int(sessions);
        if (numReqs == 0)
        {
            return API_EARGS;
        }
        request->setNumber(numReqs);

        client->getaccountdetails(request->getAccountDetails(),
                                  storage,
                                  transfer,
                                  pro,
                                  transactions,
                                  purchases,
                                  sessions,
                                  request->getAccess());
        return API_OK;
    };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::queryTransferQuota(long long size, MegaRequestListener* listener)
{
    MegaRequestPrivate* request = new MegaRequestPrivate(MegaRequest::TYPE_QUERY_TRANSFER_QUOTA, listener);
    request->setNumber(size);

    request->performRequest = [this, request]()
        {
            m_off_t size = request->getNumber();
            client->querytransferquota(size);
            return API_OK;
        };

    requestQueue.push(request);
    waiter->notify();
}

error MegaApiImpl::performRequest_changePw(MegaRequestPrivate* request)
{
            const char* oldPassword = request->getPassword();
            const char* newPassword = request->getNewPassword();
            const char* pin = request->getText();
            if (!newPassword)
            {
                return API_EARGS;
            }

            if (oldPassword && !checkPassword(oldPassword))
            {
                return API_EARGS;
            }

            return client->changepw(newPassword, pin);
}

error MegaApiImpl::performRequest_logout(MegaRequestPrivate* request)
{
            if (request->getParamType() == API_ESSL && client->retryessl)
            {
                return API_EINCOMPLETE;
            }

            if (request->getFlag())
            {
                bool keepSyncConfigsFile = request->getTransferTag() != 0;

                client->logout(keepSyncConfigsFile, [this, request](error result) {
                    LOG_debug << "executing logout completion, error: " << result;  // track possible lack of logout callbacks
                    logout_result(result, request);
                });
            }
            else
            {
                client->locallogout(false, true);
                client->restag = request->getTag();
                logout_result(API_OK, request);
            }
            return API_OK;
}

void MegaApiImpl::getNodeAttribute(std::variant<MegaNode*, MegaHandle> nodeOrHandle,
                                   int type,
                                   const char* dstFilePath,
                                   MegaRequestListener* listener)
{
    MegaRequestPrivate* request = new MegaRequestPrivate(MegaRequest::TYPE_GET_ATTR_FILE, listener);
    if (dstFilePath)
    {
        string path(dstFilePath);
        int c = path[path.size() - 1];

        if ((c == '/') || (c == '\\'))
        {
            const char* base64Handle = std::visit(
                [](auto&& arg) -> const char*
                {
                    using T = std::decay_t<decltype(arg)>;
                    if constexpr (std::is_same_v<T, MegaNode*>)
                    {
                        return arg ? arg->getBase64Handle() : nullptr;
                    }
                    else
                    {
                        return handleToBase64(arg);
                    }
                },
                nodeOrHandle);

            if (base64Handle)
            {
                path.append(base64Handle);
                path.push_back(static_cast<char>('0' + type));
                path.append(".jpg");
                delete[] base64Handle;
            }
        }

        request->setFile(path.c_str());
    }

    request->setParamType(type);

    if (std::holds_alternative<MegaNode*>(nodeOrHandle))
    {
        MegaNode* node = std::get<MegaNode*>(nodeOrHandle);
        if (node)
        {
            request->setNodeHandle(node->getHandle());

            const char* fileAttributes = node->getFileAttrString();
            if (fileAttributes)
            {
                request->setText(fileAttributes);
                const char* nodekey = node->getBase64Key();
                request->setPrivateKey(nodekey);
                delete[] nodekey;
                delete[] fileAttributes;
            }
        }
    }
    else
    {
        MegaHandle handle = std::get<MegaHandle>(nodeOrHandle);
        request->setNodeHandle(handle);
    }

    request->performRequest = [this, request]()
    {
        const char* dstFilePath = request->getFile();
        int type = request->getParamType();
        handle h = request->getNodeHandle();
        const char* fa = request->getText();
        const char* base64key = request->getPrivateKey();

        std::shared_ptr<Node> node = client->nodebyhandle(h);

        if (!dstFilePath || (!fa && !node) || (fa && (!base64key || ISUNDEF(h))))
        {
            return API_EARGS;
        }

        string fileattrstring;
        string key;
        if (!fa)
        {
            fileattrstring = node->fileattrstring;
            key = node->nodekey();
        }
        else
        {
            fileattrstring = fa;

            byte nodekey[FILENODEKEYLENGTH];
            if (Base64::atob(base64key, nodekey, sizeof nodekey) != sizeof nodekey)
            {
                return API_EKEY;
            }
            key.assign((const char*)nodekey, sizeof nodekey);
        }

        error e = client->getfa(h, &fileattrstring, key, (fatype)type);
        if (e == API_EEXIST)
        {
            e = API_OK;
            int prevtag = client->restag;
            MegaRequestPrivate* req = NULL;
            while (prevtag)
            {
                if (requestMap.find(prevtag) == requestMap.end())
                {
                    LOG_err << "Invalid duplicate getattr request";
                    req = NULL;
                    e = API_EINTERNAL;
                    break;
                }

                req = requestMap.at(prevtag);
                if (!req || (req->getType() != MegaRequest::TYPE_GET_ATTR_FILE))
                {
                    LOG_err << "Invalid duplicate getattr type";
                    req = NULL;
                    e = API_EINTERNAL;
                    break;
                }

                prevtag = int(req->getNumber());
            }

            if (req)
            {
                LOG_debug << "Duplicate getattr detected";
                req->setNumber(request->getTag());
            }
        }

        return e;
    };

    requestQueue.push(request);
    waiter->notify();
}

error MegaApiImpl::performRequest_getAttrUser(MegaRequestPrivate* request)
{
            attr_t type = static_cast<attr_t>(request->getParamType());
            if (type == ATTR_UNKNOWN)
            {
                return API_EARGS;
            }
            const char* value = request->getFile();
            const char *email = request->getEmail();
            const char *ph = request->getSessionKey();

            string attrname = MegaApiImpl::userAttributeToString(type);
            char scope = MegaApiImpl::userAttributeToScope(type);

            if ((!client->loggedin() && ph == NULL) || (ph && !ph[0]))
            {
                return API_EARGS;
            }

            User *user = email ? client->finduser(email, 0) : client->finduser(client->me, 0);

            if (!user)  // email/handle not found among (ex)contacts
            {
                if (scope != ATTR_SCOPE_PUBLIC_UNENCRYPTED &&
                    scope != ATTR_SCOPE_PROTECTED_UNENCRYPTED)
                {
                    LOG_warn << "Cannot retrieve private/protected attributes from users other than yourself.";
                    return API_EACCESS;
                }

                getUserAttr(email, type, ph, request);
                return API_OK;
            }

            bool externalDriveRequest = type == ATTR_DEVICE_NAMES && request->getFlag();
            if (attrname.empty()    // unknown attribute type
                    || (type == ATTR_AVATAR && !value)  // no destination file for avatar
                    || (externalDriveRequest && !value)) // no drive path to look for drive-id
            {
                return API_EARGS;
            }

            // if attribute is private and user is not logged in user...
            if (scope == ATTR_SCOPE_PRIVATE_ENCRYPTED && user->userhandle != client->me)
            {
                return API_EACCESS;
            }

            if (externalDriveRequest)
            {
                // check if drive-id already exists
                handle driveId;
                error e = readDriveId(*client->fsaccess, value, driveId);
                if (e != API_OK)
                {
                    return e;
                }
                request->setNodeHandle(driveId);
            }

            getUserAttr(user, type, request);
            return API_OK;
}

error MegaApiImpl::performRequest_setAttrUser(MegaRequestPrivate* request)
{
    attr_t type = static_cast<attr_t>(request->getParamType());
    if (type == ATTR_UNKNOWN)
    {
        return API_EARGS;
    }
            const char* file = request->getFile();
            const char* value = request->getText();

            char scope = MegaApiImpl::userAttributeToScope(type);
            string attrname = MegaApiImpl::userAttributeToString(type);
            if (attrname.empty())   // unknown attribute type
            {
                return API_EARGS;
            }

            User *ownUser = client->finduser(client->me);
            if (!ownUser)
            {
                assert(false && "Setting attribute without having logged in");
                return API_EACCESS;
            }

            std::function<void(Error)> putuaCompletion = [this, request](Error e)
            {
                fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e));
            };

            string attrvalue;

            if (type == ATTR_KEYRING                ||
                    type == ATTR_KEYS               ||
                    User::isAuthring(type)          ||
                    type == ATTR_CU25519_PUBK       ||
                    type == ATTR_ED25519_PUBK       ||
                    type == ATTR_SIG_CU255_PUBK     ||
                    type == ATTR_SIG_RSA_PUBK)
            {
                return API_EACCESS;
            }
            else if (type == ATTR_MY_BACKUPS_FOLDER)
            {
                LOG_err << "Cannot set 'My backups' folder attribute directly. Please, use MegaApi::setMyBackupsFolder";
                return API_EACCESS;
            }
            else if (type == ATTR_AVATAR)
            {
                // read the attribute value from file
                if (file)
                {
                    auto localpath = LocalPath::fromAbsolutePath(file);

                    auto f = fsAccess->newfileaccess();
                    if (!f->fopen(localpath, OPEN_RDONLY, FSLogging::logOnError))
                    {
                        return API_EREAD;
                    }

                    if (!f->fread(&attrvalue, unsigned(f->size), 0, 0, FSLogging::logOnError))
                    {
                        return API_EREAD;
                    }
                    f.reset();

                    client->putua(type, (byte *)attrvalue.data(), unsigned(attrvalue.size()), -1, UNDEF, 0, 0, std::move(putuaCompletion));
                    return API_OK;
                }
                else    // removing current attribute's value
                {
                    client->putua(type, nullptr, 0, -1, UNDEF, 0, 0, std::move(putuaCompletion));
                    return API_OK;
                }
            }
            else if (scope == ATTR_SCOPE_PRIVATE_ENCRYPTED)
            {
                if (type == ATTR_DEVICE_NAMES && request->getFlag()) // external drive
                {
                    const char *pathToDrive = request->getFile();
                    if (!pathToDrive)
                    {
                        return API_EARGS;
                    }

                    // check if the drive id is already created
                    // read <pathToDrive>/.megabackup/drive-id
                    handle driveId;
                    error e = readDriveId(*client->fsaccess, pathToDrive, driveId);

                    if (e == API_ENOENT)
                    {
                        // generate new id
                        driveId = generateDriveId(client->rng);
                        // write <pathToDrive>/.megabackup/drive-id
                        e = writeDriveId(*client->fsaccess, pathToDrive, driveId);
                    }

                    if (e != API_OK)
                        return e;

                    MegaStringMapPrivate stringMap;
                    const char *driveName = request->getName();
                    string buf = driveName ? driveName : "";
                    stringMap.set(Base64Str<MegaClient::DRIVEHANDLE>(driveId), Base64::btoa(buf).c_str());
                    request->setMegaStringMap(&stringMap); // makes a copy
                }

                MegaStringMap *stringMap = request->getMegaStringMap();
                if (!stringMap)
                {
                    return API_EARGS;
                }

                string_map destination;
                if (type == ATTR_ALIAS
                        || type == ATTR_CAMERA_UPLOADS_FOLDER
                        || type == ATTR_DEVICE_NAMES
                        || type == ATTR_CC_PREFS
                        || type == ATTR_APPS_PREFS)
                {
                    const UserAttribute* attribute = ownUser->getAttribute(type);
                    if (!attribute || !attribute->isValid()) // not fetched yet or outdated
                    {
                        // always get updated value before update it
                        getUserAttr(ownUser, type, request);
                        return API_OK;
                    }
                    else if (auto old = tlv::containerToRecords(attribute->value(), client->key))
                    {
                        destination.swap(*old);
                    }
                }

                const string_map *newValuesMap = static_cast<MegaStringMapPrivate*>(stringMap)->getMap();
                unique_ptr<string_map> prefixedValueMap;

                if (type == ATTR_DEVICE_NAMES)
                {
                    // allow only unique names for Devices and Drives
                    if (haveDuplicatedValues(destination, *newValuesMap))
                    {
                        LOG_err << "Attribute " << User::attr2string(type) << " attempted to add duplicated value (1): "
                            << Base64::atob(newValuesMap->begin()->second); // will only have a single value
                        return API_EEXIST;
                    }

                    if (request->getFlag()) // external drive
                    {
                        string prefix = User::attributePrefixInTLV(ATTR_DEVICE_NAMES, true);
                        prefixedValueMap = std::make_unique<string_map>();
                        for_each(newValuesMap->begin(), newValuesMap->end(),
                            [&prefixedValueMap, &prefix](const string_map::value_type& a) {prefixedValueMap->emplace(prefix + a.first, a.second); });
                        newValuesMap = prefixedValueMap.get();
                    }
                }

                if (User::mergeUserAttribute(type, *newValuesMap, destination))
                {
                    client->putua(type,
                                  std::move(destination),
                                  -1,
                                  UNDEF,
                                  0,
                                  0,
                                  std::move(putuaCompletion));
                }
                else
                {
                    // no changes, current value equal to new value
                    LOG_debug << "Attribute " << User::attr2string(type) << " not changed, already up to date";
                    fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(API_OK));
                }
                return API_OK;
            }
            else if (scope == ATTR_SCOPE_PRIVATE_UNENCRYPTED)
            {
                if (type == ATTR_LANGUAGE)
                {
                    if (!value)
                    {
                        return API_EARGS;
                    }

                    string code;
                    if (!getLanguageCode(value, &code))
                    {
                        return API_ENOENT;
                    }

                    client->putua(type, (byte *)code.data(), unsigned(code.length()), -1, UNDEF, 0, 0, [putuaCompletion, request, this](Error e)
                    {
                        // if user just set the preferred language... change the GET param to the new language
                        if (e == API_OK)
                        {
                            setLanguage(request->getText());
                        }

                        putuaCompletion(e);
                    });
                    return API_OK;
                }
                else if (type == ATTR_PWD_REMINDER)
                {
                    if (request->getNumDetails() == 0)  // nothing to be changed
                    {
                        return API_EARGS;
                    }

                    // always get updated value before update it
                    getUserAttr(ownUser, type, request);
                    return API_OK;
                }
                else if ((type == ATTR_DISABLE_VERSIONS) || (type == ATTR_NO_CALLKIT) ||
                         (type == ATTR_CONTACT_LINK_VERIFICATION) ||
                         (type == ATTR_VISIBLE_WELCOME_DIALOG) ||
                         (type == ATTR_VISIBLE_TERMS_OF_SERVICE))
                {
                    if (!value || strlen(value) != 1 || (value[0] != '0' && value[0] != '1'))
                    {
                        return API_EARGS;
                    }

                    client->putua(type, (byte*)value, 1, -1, UNDEF, 0, 0, std::move(putuaCompletion));
                }
                else if (type == ATTR_RUBBISH_TIME || type == ATTR_LAST_PSA)
                {
                    if (!value || !value[0])
                    {
                        return API_EARGS;
                    }

                    char *endptr;
                    m_off_t number = strtoll(value, &endptr, 10);
                    if (endptr == value || *endptr != '\0' || number == LLONG_MAX || number == LLONG_MIN || number < 0)
                    {
                        return API_EARGS;
                    }

                    string tmp(value);
                    client->putua(type, (byte *)tmp.data(), unsigned(tmp.size()), -1, UNDEF, 0, 0, std::move(putuaCompletion));
                }
                else if (type == ATTR_COOKIE_SETTINGS)
                {
                    setCookieSettings_sendPendingRequests(request);
                }
                else if (type == ATTR_PUSH_SETTINGS)
                {
                    const MegaPushNotificationSettingsPrivate *settings = (MegaPushNotificationSettingsPrivate*)(request->getMegaPushNotificationSettings());
                    if (!settings)
                    {
                        return API_EARGS;
                    }

                    string settingsJson = settings->generateJson();
                    if (settingsJson.empty())
                    {
                        return API_EARGS;
                    }

                    client->putua(type, (byte *)settingsJson.data(), unsigned(settingsJson.size()), -1, UNDEF, 0, 0, std::move(putuaCompletion));
                }
                else if (type == ATTR_ENABLE_TEST_NOTIFICATIONS)
                {
                    performRequest_enableTestNotifications(request);
                }
                else if (type == ATTR_LAST_READ_NOTIFICATION)
                {
                    performRequest_setLastReadNotification(request);
                }
                else if (type == ATTR_LAST_ACTIONED_BANNER)
                {
                    performRequest_setLastActionedBanner(request);
                }
                else if (type == ATTR_LAST_PURGE_ACKNOWLEDGED)
                {
                    performRequest_setLastPurgeAcknowledged(request);
                }
                else if (type == ATTR_ENABLE_TEST_SURVEYS)
                {
                    performRequest_enableTestSurveys(request);
                }
                else if (type == ATTR_DEV_OPT)
                {
                    if (!value)
                    {
                        return API_EARGS;
                    }

                    client->putua(type,
                                  reinterpret_cast<const byte*>(value),
                                  static_cast<unsigned>(strlen(value)),
                                  -1,
                                  UNDEF,
                                  0,
                                  0,
                                  std::move(putuaCompletion));
                }
                else
                {
                    return API_EARGS;
                }
            }
            else    // any other type of attribute
            {
                if (!value)
                {
                    return API_EARGS;
                }

                client->putua(type, (byte *)value, unsigned(strlen(value)), -1, UNDEF, 0, 0, std::move(putuaCompletion));
                return API_OK;
            }

            return API_OK;
}

void MegaApiImpl::getUserEmail(MegaHandle h, MegaRequestListener* listener)
{
    MegaRequestPrivate* request = new MegaRequestPrivate(MegaRequest::TYPE_GET_USER_EMAIL, listener);
    request->setNodeHandle(h);

    request->performRequest = [this, request]()
        {
            handle uh = request->getNodeHandle();
            if (uh == INVALID_HANDLE)
            {
                return API_EARGS;
            }

            char uid[12];
            Base64::btoa((byte*)&uh, MegaClient::USERHANDLE, uid);
            uid[11] = 0;

            client->getUserEmail(uid);
            return API_OK;
        };

    requestQueue.push(request);
    waiter->notify();
}

error MegaApiImpl::performRequest_setAttrFile(MegaRequestPrivate* request)
{
    const char* srcFilePath = request->getFile();
    int type = request->getParamType();
    std::shared_ptr<Node> node = request->getNodeHandle() == INVALID_HANDLE ?
                                     nullptr :
                                     client->nodebyhandle(request->getNodeHandle());
    MegaHandle fileattrhandle = (uint64_t)request->getNumber();
    auto bu =
        static_cast<MegaBackgroundMediaUploadPrivate*>(request->getMegaBackgroundMediaUploadPtr());

    if (!srcFilePath)
    {
        if (!node || fileattrhandle == INVALID_HANDLE)
        {
            return API_EARGS;
        }

        string fileattr;
        appendFileAttribute(fileattr, type, fileattrhandle);
        client->putFileAttributes(node->nodehandle, fatype(type), fileattr, request->getTag());
        return API_OK;
    }

    if (!node == !bu)
    {
        return API_EARGS;
    }

    auto localpath = LocalPath::fromAbsolutePath(srcFilePath);

    std::unique_ptr<string> attributedata(new string);
    std::unique_ptr<FileAccess> f(fsAccess->newfileaccess());
    if (!f->fopen(localpath, OPEN_RDONLY, FSLogging::logOnError))
    {
        return API_EREAD;
    }

    // make the string a little bit larger initially with SymmCipher::BLOCKSIZE to avoid heap
    // activity growing it for the encryption
    attributedata->reserve(size_t(f->size + SymmCipher::BLOCKSIZE));
    if (!f->fread(attributedata.get(), unsigned(f->size), 0, 0, FSLogging::logOnError))
    {
        return API_EREAD;
    }

    return client->putfa(NodeOrUploadHandle(node ? node->nodeHandle() : NodeHandle()),
                         static_cast<fatype>(type),
                         bu ? bu->nodecipher(client) : node->nodecipher(),
                         request->getTag(),
                         std::move(attributedata));
}

error MegaApiImpl::performRequest_setAttrNode(MegaRequestPrivate* request)
{
    constexpr char logPre[] = "performRequest_setAttrNode. ";
    const handle h = request->getNodeHandle();
    if (h == INVALID_HANDLE)
    {
        LOG_err << logPre << "invalid handle";
        return API_EARGS;
    }

    error e = API_OK;
    std::shared_ptr<Node> node = client->nodebyhandle(h);
    if (!node)
    {
        LOG_err << logPre << "node not found with handle" << Base64Str<MegaClient::NODEHANDLE>(h);
        return API_EARGS;
    }

    if (!client->checkaccess(node.get(), FULL))
    {
        LOG_err << logPre
                << "unexpected access level for node: " << Base64Str<MegaClient::NODEHANDLE>(h);
        return API_EACCESS;
    }

    attr_map attrUpdates;
    if (const auto isOfficial = request->getFlag(); isOfficial)
    {
        if (int type = request->getParamType(); type == MegaApi::NODE_ATTR_DURATION)
        {
            int secs = int(request->getNumber());
            if (node->type != FILENODE || secs < MegaNode::INVALID_DURATION)
            {
                LOG_err << logPre << "invalid duration: " << secs;
                return API_EARGS;
            }

            if (secs == MegaNode::INVALID_DURATION)
            {
                attrUpdates['d'] = "";
            }
            else
            {
                string attrVal;
                Base64::itoa(secs, &attrVal);
                if (attrVal.size())
                {
                    attrUpdates['d'] = attrVal;
                }
            }
        }
        else if (type == MegaApi::NODE_ATTR_COORDINATES)
        {
            if (node->type != FILENODE)
            {
                LOG_err << logPre << "invalid nodetype: " << node->type;
                return API_EARGS;
            }

            int longitude = request->getNumDetails();
            int latitude = request->getTransferTag();
            int unshareable = request->getAccess();

            e = updateAttributesMapWithCoordinates(attrUpdates,
                                                   latitude,
                                                   longitude,
                                                   !!unshareable,
                                                   client);
            if (e != API_OK)
            {
                return e;
            }
        }
        else if (type == MegaApi::NODE_ATTR_ORIGINALFINGERPRINT)
        {
            nameid nid = AttrMap::string2nameid("c0");
            if (!request->getText())
            {
                attrUpdates[nid] = "";
            }
            else
            {
                attrUpdates[nid] = request->getText();
            }
        }
        else if (type == MegaApi::NODE_ATTR_LABEL || type == MegaApi::NODE_ATTR_FAV ||
                 type == MegaApi::NODE_ATTR_SEN)
        {
            std::shared_ptr<Node> current = node;
            bool remove = false;
            nameid nid = 0;
            int value = 0;
            if (type == MegaApi::NODE_ATTR_LABEL)
            {
                value = request->getNumDetails();
                if (value < LBL_UNKNOWN || value > LBL_GREY)
                {
                    LOG_err << logPre << "invalid label: " << value;
                    return API_EARGS;
                }

                nid = AttrMap::string2nameid("lbl");
                remove = (value == LBL_UNKNOWN);
            }
            else if (type == MegaApi::NODE_ATTR_SEN)
            {
                nid = AttrMap::string2nameid("sen");
                remove = !request->getNumDetails();
                value = 1;
            }
            else
            {
                nid = AttrMap::string2nameid("fav");
                remove = !request->getNumDetails();
                value = 1;
            }

            if (remove)
            {
                attrUpdates[nid] = "";
            }
            else
            {
                attrUpdates[nid] = std::to_string(value);
            }

            // update file versions if any
            if (current->type == FILENODE)
            {
                sharedNode_list childrens = client->getChildren(current.get());
                while (childrens.size())
                {
                    assert(childrens.size() == 1); // versions are 1-child chains
                    std::shared_ptr<Node> n = *childrens.begin();
                    client->setattr(n,
                                    attr_map(attrUpdates),
                                    nullptr,
                                    false); // no callback for these
                    childrens = client->getChildren(n.get());
                }
            }

            return client->setattr(
                current,
                std::move(attrUpdates),
                [request, this](NodeHandle h, Error e)
                {
                    request->setNodeHandle(h.as8byte());
                    fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e));
                },
                false);
        }
        else if (bool isTypeS4 = (type == MegaApi::NODE_ATTR_S4);
                 isTypeS4 || type == MegaApi::NODE_ATTR_DESCRIPTION)
        {
            const char* attrValue = request->getText();
            if (type == MegaApi::NODE_ATTR_DESCRIPTION && attrValue &&
                strlen(attrValue) > MegaApi::MAX_NODE_DESCRIPTION_SIZE)
            {
                LOG_err << logPre << "invalid attrValue len";
                return API_EARGS;
            }

            const char* attributeName = isTypeS4 ? "s4" : MegaClient::NODE_ATTRIBUTE_DESCRIPTION;
            attrUpdates[AttrMap::string2nameid(attributeName)] = attrValue ? attrValue : "";
        }
        else
        {
            LOG_err << logPre << "invalid attrType: " << type;
            return API_EARGS;
        }
    }
    else // custom attribute, not official
    {
        const char* attrName = request->getName();
        const char* attrValue = request->getText();

        if (!attrName || !attrName[0] || strlen(attrName) > 7)
        {
            LOG_err << logPre << "invalid attrName: " << (attrName && attrName[0] ? attrName : "");
            return API_EARGS;
        }

        string sname = attrName;
        LocalPath::utf8_normalize(&sname);
        sname.insert(0, "_");
        nameid attr = AttrMap::string2nameid(sname.c_str());

        if (attrValue)
        {
            string svalue = attrValue;
            LocalPath::utf8_normalize(&svalue);
            attrUpdates[attr] = svalue;
        }
        else
        {
            attrUpdates[attr] = "";
        }
    }

    if (!e && !attrUpdates.empty())
    {
        e = client->setattr(
            node,
            std::move(attrUpdates),
            [request, this](NodeHandle h, Error e)
            {
                request->setNodeHandle(h.as8byte());
                fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e));
            },
            false);
    }

    return e;
}

void MegaApiImpl::getFavourites(MegaNode* node, int count, MegaRequestListener* listener)
{
    MegaRequestPrivate* request = new MegaRequestPrivate(MegaRequest::TYPE_GET_ATTR_NODE, listener);
    if (node)
    {
        request->setNodeHandle(node->getHandle());
    }
    else
    {
        request->setNodeHandle(UNDEF);
    }

    request->setParamType(MegaApi::NODE_ATTR_FAV);
    request->setNumDetails(count);

    request->performRequest = [this, request]()
        {
            int type = request->getParamType();

            if (type == MegaApi::NODE_ATTR_FAV)
            {
                int count = request->getNumDetails();
                MegaHandle folderHandle = request->getNodeHandle();
                std::shared_ptr<Node> node;
                if (!ISUNDEF(folderHandle))
                {
                    node = client->nodebyhandle(folderHandle);
                    if (!node)
                    {
                        return API_ENOENT;
                    }
                    if (node->type != FOLDERNODE)
                    {
                        return API_EARGS;
                    }
                }
                else
                {
                    node = client->nodeByHandle(client->mNodeManager.getRootNodeFiles());
                    if (!node)
                    {
                        LOG_debug << "Lookup of files root node failed";
                        return API_ENOENT;
                    }
                }

                // Check if 'node' is favourite, DB query starts at 'node' children
                std::vector<NodeHandle> favouriteNodes;
                nameid nid = AttrMap::string2nameid("fav");
                auto attrMapIterator = node->attrs.map.find(nid);
                if (attrMapIterator != node->attrs.map.end() && attrMapIterator->second == "1")
                {
                    favouriteNodes.push_back(node->nodeHandle());
                }

                if (count != 1 || favouriteNodes.empty())
                {
                    std::vector<NodeHandle> favs =
                        client->mNodeManager.getFavouritesNodeHandles(node->nodeHandle(),
                                                                      static_cast<uint32_t>(count));
                    favouriteNodes.insert(favouriteNodes.end(), favs.begin(), favs.end());
                }

                std::vector<handle> handles;
                for (const NodeHandle& nodeHandle : favouriteNodes)
                {
                    handles.push_back(nodeHandle.as8byte());
                }

                request->setMegaHandleList(handles);
                fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(API_OK));
            }
            return API_OK;
        };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::cancelGetNodeAttribute(MegaNode* node, int type, MegaRequestListener* listener)
{
    MegaRequestPrivate* request = new MegaRequestPrivate(MegaRequest::TYPE_CANCEL_ATTR_FILE, listener);
    request->setParamType(type);
    if (node)
    {
        request->setNodeHandle(node->getHandle());
        const char* fileAttributes = node->getFileAttrString();
        if (fileAttributes)
        {
            request->setText(fileAttributes);
            delete[] fileAttributes;
        }
    }

    request->performRequest = [this, request]()
        {
            int type = request->getParamType();
            handle h = request->getNodeHandle();
            const char *fa = request->getText();

            std::shared_ptr<Node> node = client->nodebyhandle(h);

            if((!fa && !node) || (fa && ISUNDEF(h)))
            {
                return API_EARGS;
            }

            string fileattrstring = fa ? string(fa) : node->fileattrstring;

            error e = client->getfa(h, &fileattrstring, string(), (fatype)type, 1);
            if (!e)
            {
                std::map<int, MegaRequestPrivate*>::iterator it = requestMap.begin();
                while(it != requestMap.end())
                {
                    MegaRequestPrivate *r = it->second;
                    it++;
                    if (r->getType() == MegaRequest::TYPE_GET_ATTR_FILE &&
                        r->getParamType() == request->getParamType() &&
                        r->getNodeHandle() == request->getNodeHandle())
                    {
                        fireOnRequestFinish(r, std::make_unique<MegaErrorPrivate>(API_EINCOMPLETE));
                    }
                }
                fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e));
            }
            return e;
        };

    requestQueue.push(request);
    waiter->notify();
}

error MegaApiImpl::performRequest_retryPendingConnections(MegaRequestPrivate* request)
{
            bool disconnect = request->getFlag();
            bool includexfers = request->getNumber() != 0;
            const char *dnsservers = request->getText();
            error result{API_OK};

            client->abortbackoff(includexfers);
            if (disconnect)
            {
                client->disconnect();

                if (dnsservers)
                {
                    if (!httpio->setdnsservers(dnsservers))
                    {
                        LOG_warn << "libcurl does not have support for a DNS resolver backend. "
                                    "Build libcurl with c-ares support.";
                        result = API_ENOENT;
                    }
                }
            }

            fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(result));
            return result;
}

void MegaApiImpl::inviteContact(const char* email, const char* message, int action, MegaHandle contactLink, MegaRequestListener* listener)
{
    MegaRequestPrivate* request = new MegaRequestPrivate(MegaRequest::TYPE_INVITE_CONTACT, listener);
    request->setNumber(action);
    request->setEmail(email);
    request->setText(message);
    request->setNodeHandle(contactLink);

    request->performRequest = [this, request]()
        {
            const char *email = request->getEmail();
            const char *message = request->getText();
            int action = int(request->getNumber());
            MegaHandle contactLink = request->getNodeHandle();

            if(client->loggedin() != FULLACCOUNT)
            {
                return API_EACCESS;
            }

            if(!email || !client->finduser(client->me)->email.compare(email))
            {
                return API_EARGS;
            }

            if (action != OPCA_ADD && action != OPCA_REMIND && action != OPCA_DELETE)
            {
                return API_EARGS;
            }

            if (action == OPCA_ADD && client->findpcr(string{email}))
            {
                return API_EEXIST;
            }

            client->setpcr(email, (opcactions_t)action, message, NULL, contactLink);
            return API_OK;
        };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::replyContactRequest(const MegaContactRequest* r,
                                      int action,
                                      MegaRequestListener* listener)
{
    MegaRequestPrivate* request = new MegaRequestPrivate(MegaRequest::TYPE_REPLY_CONTACT_REQUEST, listener);
    if (r)
    {
        request->setNodeHandle(r->getHandle());
    }

    request->setNumber(action);

    request->performRequest = [this, request]()
        {
            handle h = request->getNodeHandle();
            int action = int(request->getNumber());

            if(h == INVALID_HANDLE || action < 0 || action > MegaContactRequest::REPLY_ACTION_IGNORE)
            {
                return API_EARGS;
            }

            client->updatepcr(h, (ipcactions_t)action);
            return API_OK;
        };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::removeContact(MegaUser* user, MegaRequestListener* listener)
{
    MegaRequestPrivate* request = new MegaRequestPrivate(MegaRequest::TYPE_REMOVE_CONTACT, listener);
    if (user)
    {
        request->setEmail(user->getEmail());
    }

    request->performRequest = [this, request]()
        {
            const char *email = request->getEmail();
            User *u = client->finduser(email);
            if(!u || u->show == HIDDEN || u->userhandle == client->me) { return API_EARGS; }
            if (client->mBizMode == BIZ_MODE_SUBUSER && u->mBizMode != BIZ_MODE_UNKNOWN)
            {
                return API_EMASTERONLY;
            }

            return client->removecontact(email, HIDDEN);
        };

    requestQueue.push(request);
    waiter->notify();
}

error MegaApiImpl::performRequest_createAccount(MegaRequestPrivate* request)
{
            const char *email = request->getEmail();
            const char *password = request->getPassword();
            const char *name = request->getName();
            const char *lastname = request->getText();
            const char *sid = request->getSessionKey();
            bool resumeProcess = (request->getParamType() == MegaApi::RESUME_ACCOUNT);   // resume existing ephemeral account
            bool cancelProcess = (request->getParamType() == MegaApi::CANCEL_ACCOUNT);
            bool createEphemeralPlusPlus = (request->getParamType() == MegaApi::CREATE_EPLUSPLUS_ACCOUNT);   // create ephemeral++ account
            bool resumeEphemeralPlusPlus = (request->getParamType() == MegaApi::RESUME_EPLUSPLUS_ACCOUNT);   // resume existing ephemeral++ account
            handle lastPublicHandle = request->getNodeHandle();
            int lastPublicHandleType = request->getAccess();
            int64_t lastAccessTimestamp =request->getTransferredBytes();

            if (!ISUNDEF(lastPublicHandle)
                    && ((lastPublicHandleType <= mega::MegaApi::AFFILIATE_TYPE_INVALID
                            || lastPublicHandleType > mega::MegaApi::AFFILIATE_TYPE_CONTACT)
                        || !lastAccessTimestamp))
            {
                return API_EARGS;
            }

            if ((!resumeProcess && !cancelProcess && !resumeEphemeralPlusPlus && !createEphemeralPlusPlus &&
                  (!email || !name || !password)) ||
                 ((resumeProcess || resumeEphemeralPlusPlus) && !sid) ||
                 (createEphemeralPlusPlus && !(name && lastname)))
            {
                return API_EARGS;
            }

            byte pwbuf[SymmCipher::KEYLENGTH];
            handle uh = UNDEF;
            if (resumeProcess)
            {
                size_t pwkeyLen = strlen(sid);
                size_t pwkeyLenExpected = SymmCipher::KEYLENGTH * 4 / 3 + 3 + 10;
                if (pwkeyLen != pwkeyLenExpected ||
                        Base64::atob(sid, (byte*) &uh, sizeof uh) != sizeof uh ||
                        uh == UNDEF || sid[11] != '#' ||
                        Base64::atob(sid + 12, pwbuf, sizeof pwbuf) != sizeof pwbuf)
                {
                    return API_EARGS;
                }
            }

            if (cancelProcess)
            {
                client->cancelsignup();
            }
            else
            {
                int reqtag = request->getTag();
                requestMap.erase(reqtag);

                abortPendingActions();

                requestMap[reqtag] = request;

                client->locallogout(false, true);

                if (resumeProcess)
                {
                    client->resumeephemeral(uh, pwbuf);
                }
                else if (resumeEphemeralPlusPlus)
                {
                    client->resumeephemeralPlusPlus(Base64::atob(string(sid)));
                }
                else if (createEphemeralPlusPlus)
                {
                    client->createephemeralPlusPlus();
                }
                else
                {
                    assert(request->getParamType() == MegaApi::CREATE_ACCOUNT);
                    client->createephemeral();
                }
            }

            return API_OK;
}

error MegaApiImpl::performRequest_sendSignupLink(MegaRequestPrivate* request)
{
            const char *email = request->getEmail();
            const char *name = request->getName();
            if (!email || !name)
            {
                return API_EARGS;
            }

            if (client->loggedin() != EPHEMERALACCOUNT && client->loggedin() != EPHEMERALACCOUNTPLUSPLUS)
            {
                return API_EACCESS;
            }

            if (client->accountversion != 2)
            {
                return API_EINTERNAL;
            }

            client->resendsignuplink2(email, name);
            return API_OK;
}

void MegaApiImpl::querySignupLink(const char* link, MegaRequestListener* listener)
{
    MegaRequestPrivate* request = new MegaRequestPrivate(MegaRequest::TYPE_QUERY_SIGNUP_LINK, listener);
    request->setLink(link);

    request->performRequest = [this, request]()
        {
            const char *link = request->getLink();
            if(!link)
            {
                return API_EARGS;
            }

            const char* ptr = link;
            const char* tptr;

            // is it a link to confirm the account? ie. https://mega.app/#confirm<code_in_B64>
            tptr = strstr(ptr, MegaClient::confirmLinkPrefix());
            if (tptr)
            {
                ptr = tptr + strlen(MegaClient::confirmLinkPrefix());

                string code = Base64::atob(string(ptr));
                if (!code.empty())
                {
                    if (code.find("ConfirmCodeV2") != string::npos)
                    {
                        // “ConfirmCodeV2” (13B) || Email Confirmation Token (15B) || Email (>=5B) || \t || Fullname || Hash (8B)
                        size_t posEmail = 13 + 15;
                        size_t endEmail = code.find("\t", posEmail);
                        if (endEmail != string::npos)
                        {
                            string email = code.substr(posEmail, endEmail - posEmail);
                            request->setEmail(email.c_str());
                            request->setName(code.substr(endEmail + 1, code.size() - endEmail - 9).c_str());

                            sessiontype_t session = client->loggedin();
                            if (session == FULLACCOUNT)
                            {
                                return (client->ownuser()->email == email) ? API_EEXPIRED : API_EACCESS;
                            }
                            else    // not-logged-in / ephemeral account / partially confirmed
                            {
                                client->confirmsignuplink2((const byte*)code.data(), unsigned(code.size()));
                                return API_OK;
                            }
                        }
                    }
                }
            }
            // is it a new singup link? ie. https://mega.app/#newsignup<code_in_B64>
            else if ((tptr = strstr(ptr, MegaClient::newsignupLinkPrefix())) != nullptr)
            {
                ptr = tptr + strlen(MegaClient::newsignupLinkPrefix());

                unsigned len = static_cast<unsigned>(
                    (strlen(link) - static_cast<size_t>(ptr - link)) * 3 / 4 + 4);
                byte *c = new byte[len];
                len = static_cast<unsigned>(Base64::atob(ptr, c, static_cast<int>(len)));

                if (len > 8)
                {
                    // extract email and email_hash from link
                    byte *email = c;
                    byte *sha512bytes = c+len-8;    // last 11 chars

                    // get the hash for the received email
                    Hash sha512;
                    sha512.add(email, len-8);
                    string sha512str;
                    sha512.get(&sha512str);

                    // and finally check it
                    if (memcmp(sha512bytes, sha512str.data(), 8) == 0)
                    {
                        email[len-8] = '\0';
                        request->setEmail((const char *)email);
                        delete[] c;

                        fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(API_OK));
                        return API_OK;
                    }
                }

                delete[] c;
            }

            return API_EARGS;
        };

    requestQueue.push(request);
    waiter->notify();
}

error MegaApiImpl::performRequest_confirmAccount(MegaRequestPrivate* request)
{
            const char* link = request->getLink();
            if (!link)
            {
                return API_EARGS;
            }

            const char* ptr = link;
            const char* tptr;

            tptr = strstr(ptr, MegaClient::confirmLinkPrefix());
            if (tptr)
                ptr = tptr + strlen(MegaClient::confirmLinkPrefix());

            error e = API_OK;
            string code = Base64::atob(string(ptr));
            if (code.find("ConfirmCodeV2") != string::npos)
            {
                // “ConfirmCodeV2” (13B) || Email Confirmation Token (15B) || Email (>=5B) || \t || Fullname || Hash (8B)
                size_t posEmail = 13 + 15;
                size_t endEmail = code.find("\t", posEmail);
                if (endEmail != string::npos)
                {
                    string email = code.substr(posEmail, endEmail - posEmail);
                    request->setEmail(email.c_str());
                    request->setName(code.substr(endEmail + 1, code.size() - endEmail - 9).c_str());

                    sessiontype_t session = client->loggedin();
                    if (session == FULLACCOUNT)
                    {
                        e = (client->ownuser()->email == email) ? API_EEXPIRED : API_EACCESS;
                    }
                    else    // not-logged-in / ephemeral account / partially confirmed
                    {
                        client->confirmsignuplink2((const byte*)code.data(), unsigned(code.size()));
                    }
                }
                else
                {
                    e = API_EARGS;
                }
            }
            else
            {
                e = API_EARGS;
            }
            return e;
}

void MegaApiImpl::resetPassword(const char* email, bool hasMasterKey, MegaRequestListener* listener)
{
    MegaRequestPrivate* request = new MegaRequestPrivate(MegaRequest::TYPE_GET_RECOVERY_LINK, listener);
    request->setEmail(email);
    request->setFlag(hasMasterKey);

    request->performRequest = [this, request]()
        {
            const char *email = request->getEmail();
            bool hasMasterKey = request->getFlag();

            if (!email || !email[0])
            {
                return API_EARGS;
            }

            client->getrecoverylink(email, hasMasterKey);
            return API_OK;
        };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::queryRecoveryLink(const char* link, MegaRequestListener* listener)
{
    MegaRequestPrivate* request = new MegaRequestPrivate(MegaRequest::TYPE_QUERY_RECOVERY_LINK, listener);
    request->setLink(link);

    request->performRequest = [this, request]()
        {
            const char *link = request->getLink();

            const char* code;
            if (link && (code = strstr(link, MegaClient::recoverLinkPrefix())) != nullptr)
            {
                code += strlen(MegaClient::recoverLinkPrefix());
            }
            else if (link && (code = strstr(link, MegaClient::verifyLinkPrefix())) != nullptr)
            {
                code += strlen(MegaClient::verifyLinkPrefix());
            }
            else if (link && (code = strstr(link, MegaClient::cancelLinkPrefix())) != nullptr)
            {
                code += strlen(MegaClient::cancelLinkPrefix());
            }
            else
            {
                return API_EARGS;
            }

            client->queryrecoverylink(code);
            return API_OK;
        };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::confirmResetPasswordLink(const char* link, const char* newPwd, const char* masterKey, MegaRequestListener* listener)
{
    MegaRequestPrivate* request = new MegaRequestPrivate(MegaRequest::TYPE_CONFIRM_RECOVERY_LINK, listener);
    request->setLink(link);
    request->setPassword(newPwd);
    request->setPrivateKey(masterKey);

    request->performRequest = [this, request]()
        {
            const char *link = request->getLink();
            const char *newPwd = request->getPassword();
            if (!newPwd || !link)
            {
                return API_EARGS;
            }

            const char* code;
            code = strstr(link, MegaClient::recoverLinkPrefix());
            if (!code)
            {
                return API_EARGS;
            }

            code += strlen(MegaClient::recoverLinkPrefix());
            // concatenate query + confirm requests
            client->queryrecoverylink(code);
            return API_OK;
        };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::checkRecoveryKey(const char* link, const char* recoveryKey, MegaRequestListener* listener)
{
    MegaRequestPrivate* request = new MegaRequestPrivate(MegaRequest::TYPE_CHECK_RECOVERY_KEY, listener);
    request->setLink(link);
    request->setPrivateKey(recoveryKey);

    request->performRequest = [this, request]()
        {
            const char* link = request->getLink();
            if (!link)
            {
                return API_EARGS;
            }

            const char* code;
            code = strstr(link, MegaClient::recoverLinkPrefix());
            if (!code)
            {
                return API_EARGS;
            }

            code += strlen(MegaClient::recoverLinkPrefix());
            // concatenate query + confirm requests
            client->getprivatekey(code);
            return API_OK;
        };

    requestQueue.push(request);
    waiter->notify();
}

error MegaApiImpl::performRequest_getCancelLink(MegaRequestPrivate* request)
{
            if (client->loggedin() != FULLACCOUNT)
            {
                return API_EACCESS;
            }

            User *u = client->finduser(client->me);
            if (!u)
            {
                return API_ENOENT;
            }

            const char *pin = request->getText();
            client->getcancellink(u->email.c_str(), pin);
            return API_OK;
}

void MegaApiImpl::confirmCancelAccount(const char* link, const char* pwd, MegaRequestListener* listener)
{
    MegaRequestPrivate* request = new MegaRequestPrivate(MegaRequest::TYPE_CONFIRM_CANCEL_LINK, listener);
    request->setLink(link);
    request->setPassword(pwd);

    request->performRequest = [this, request]()
        {
            const char *link = request->getLink();
            const char *pwd = request->getPassword();

            if (client->loggedin() != FULLACCOUNT)
            {
                return API_EACCESS;
            }

            if (!pwd || !link)
            {
                return API_EARGS;
            }

            const char* code;
            code = strstr(link, MegaClient::cancelLinkPrefix());
            if (!code)
            {
                return API_EARGS;
            }

            if (!checkPassword(pwd))
            {
                return API_ENOENT;
            }

            code += strlen(MegaClient::cancelLinkPrefix());
            client->confirmcancellink(code);
            return API_OK;
        };

    requestQueue.push(request);
    waiter->notify();
}

error MegaApiImpl::performRequest_getChangeEmailLink(MegaRequestPrivate* request)
{
            if (client->loggedin() != FULLACCOUNT)
            {
                return API_EACCESS;
            }

            const char *email = request->getEmail();
            const char *pin = request->getText();
            if (!email)
            {
                return API_EARGS;
            }

            client->getemaillink(email, pin);
            return API_OK;
}

void MegaApiImpl::confirmChangeEmail(const char* link, const char* pwd, MegaRequestListener* listener)
{
    MegaRequestPrivate* request = new MegaRequestPrivate(MegaRequest::TYPE_CONFIRM_CHANGE_EMAIL_LINK, listener);
    request->setLink(link);
    request->setPassword(pwd);

    request->performRequest = [this, request]()
        {
            const char *link = request->getLink();
            const char *pwd = request->getPassword();

            if (client->loggedin() != FULLACCOUNT)
            {
                return API_EACCESS;
            }

            if (!pwd || !link)
            {
                return API_EARGS;
            }

            const char* code;
            code = strstr(link, MegaClient::verifyLinkPrefix());
            if (!code)
            {
                return API_EARGS;
            }

            code += strlen(MegaClient::verifyLinkPrefix());
            // concatenates query + validate pwd + confirm
            client->queryrecoverylink(code);
            return API_OK;
        };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::pauseTransfers(bool pause, int direction, MegaRequestListener* listener)
{
    MegaRequestPrivate* request = new MegaRequestPrivate(MegaRequest::TYPE_PAUSE_TRANSFERS, listener);
    request->setFlag(pause);
    request->setNumber(direction);

    request->performTransferRequest = [this, request](TransferDbCommitter& committer)
        {
            bool pause = request->getFlag();
            int direction = int(request->getNumber());
            if(direction != -1
                    && direction != MegaTransfer::TYPE_DOWNLOAD
                    && direction != MegaTransfer::TYPE_UPLOAD)
            {
                return API_EARGS;
            }

            if(direction == -1)
            {
                client->pausexfers(PUT, pause, false, committer);
                client->pausexfers(GET, pause, false, committer);
            }
            else if(direction == MegaTransfer::TYPE_DOWNLOAD)
            {
                client->pausexfers(GET, pause, false, committer);
            }
            else
            {
                client->pausexfers(PUT, pause, false, committer);
            }

            fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(API_OK));
            return API_OK;
        };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::pauseTransfer(int transferTag, bool pause, MegaRequestListener* listener)
{
    MegaRequestPrivate* request = new MegaRequestPrivate(MegaRequest::TYPE_PAUSE_TRANSFER, listener);
    request->setTransferTag(transferTag);
    request->setFlag(pause);

    request->performTransferRequest = [this, request](TransferDbCommitter& committer)
        {
            bool pause = request->getFlag();
            int transferTag = request->getTransferTag();
            MegaTransferPrivate* megaTransfer = getMegaTransferPrivate(transferTag);
            if (!megaTransfer)
            {
                return API_ENOENT;
            }

            error e = client->transferlist.pause(megaTransfer->getTransfer(), pause, committer);
            if (!e)
            {
                fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(API_OK));
            }
            return e;
        };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::resumeTransfersForNotLoggedInInstance()
{
    SdkMutexGuard g(sdkMutex);
    client->resumeTransfersForNotLoggedInInstance();
}

error MegaApiImpl::performTransferRequest_moveTransfer(MegaRequestPrivate* request, TransferDbCommitter& committer)
{
            bool automove = request->getFlag();
            int transferTag = request->getTransferTag();
            int number = int(request->getNumber());

            if (!transferTag || !number)
            {
                return API_EARGS;
            }

            MegaTransferPrivate* megaTransfer = getMegaTransferPrivate(transferTag);
            if (!megaTransfer)
            {
                return API_ENOENT;
            }

            Transfer *transfer = megaTransfer->getTransfer();
            if (!transfer)
            {
                return API_ENOENT;
            }

            if (automove)
            {
                switch (number)
                {
                    case MegaTransfer::MOVE_TYPE_UP:
                        client->transferlist.moveup(transfer, committer);
                        break;
                    case MegaTransfer::MOVE_TYPE_DOWN:
                        client->transferlist.movedown(transfer, committer);
                        break;
                    case MegaTransfer::MOVE_TYPE_TOP:
                        client->transferlist.movetofirst(transfer, committer);
                        break;
                    case MegaTransfer::MOVE_TYPE_BOTTOM:
                        client->transferlist.movetolast(transfer, committer);
                        break;
                    default:
                        return API_EARGS;
                }
            }
            else
            {
                MegaTransferPrivate* prevMegaTransfer = getMegaTransferPrivate(number);
                if (!prevMegaTransfer)
                {
                    return API_ENOENT;
                }

                Transfer *prevTransfer = prevMegaTransfer->getTransfer();
                if (!prevTransfer)
                {
                    client->transferlist.movetransfer(transfer, client->transferlist.transfers[transfer->type].begin(), committer);
                }
                else
                {
                    if (transfer->type != prevTransfer->type)
                    {
                        return API_EARGS;
                    }
                    else
                    {
                        client->transferlist.movetransfer(transfer, prevTransfer, committer);
                    }
                }
            }

            fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(API_OK));
            return API_OK;
}

void MegaApiImpl::setMaxConnections(int direction, int connections, MegaRequestListener* listener)
{
    MegaRequestPrivate* request = new MegaRequestPrivate(MegaRequest::TYPE_SET_MAX_CONNECTIONS, listener);
    request->setParamType(direction);
    request->setNumber(connections);

    request->performRequest = [this, request]()
        {
            int direction = request->getParamType();
            int connections = int(request->getNumber());

            if (connections <= 0 || (direction != -1
                    && direction != MegaTransfer::TYPE_DOWNLOAD
                    && direction != MegaTransfer::TYPE_UPLOAD))
            {
                return API_EARGS;
            }

            if (connections > static_cast<int>(MegaClient::MAX_NUM_CONNECTIONS))
            {
                return API_ETOOMANY;
            }

            error e = API_OK;
            if (direction == -1)
            {
                e = client->setmaxconnectionsandpersist(static_cast<uint8_t>(connections));
            }
            else if (direction == MegaTransfer::TYPE_DOWNLOAD)
            {
                e = client->setmaxconnectionsandpersist(GET, static_cast<uint8_t>(connections));
            }
            else
            {
                e = client->setmaxconnectionsandpersist(PUT, static_cast<uint8_t>(connections));
            }

            fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e));
            return API_OK;
        };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::getMaxTransferConnections(const direction_t direction,
                                            MegaRequestListener* const listener)
{
    auto* const request = new MegaRequestPrivate(MegaRequest::TYPE_GET_MAX_CONNECTIONS, listener);
    assert(direction == GET || direction == PUT);
    request->setParamType(direction);

    request->performRequest = [this, request]()
    {
        const auto direction = request->getParamType();
        if (direction != GET && direction != PUT)
            return API_EINTERNAL;

        request->setNumber(client->connections[direction]);

        fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(API_OK));
        return API_OK;
    };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::getMaxUploadConnections(MegaRequestListener* const listener)
{
    getMaxTransferConnections(PUT, listener);
}

void MegaApiImpl::getMaxDownloadConnections(MegaRequestListener* const listener)
{
    getMaxTransferConnections(GET, listener);
}

error MegaApiImpl::performTransferRequest_cancelTransfer(MegaRequestPrivate* request, TransferDbCommitter& committer)
{
            int transferTag = request->getTransferTag();
            MegaTransferPrivate* megaTransfer = getMegaTransferPrivate(transferTag);
            if (!megaTransfer)
            {
                return API_ENOENT;
            }

            if (megaTransfer->getType() == MegaTransfer::TYPE_LOCAL_TCP_DOWNLOAD)
            {
                return API_EACCESS;
            }

            if (megaTransfer->isFolderTransfer())
            {
                // folder upload or folder download.
                // we can do this by cancel token
                // all sub-transfers must have the same cancel token
                if (!megaTransfer->getCancelToken())
                {
                    LOG_warn << "Cancel requested for folder transfer, but it has lost its cancel token";
                    return API_EARGS;
                }
                else
                {
                    // there is a race to this point, in theory the operation could have just completed successfully on the thread
                    megaTransfer->stopRecursiveOperationThread();

                    // mark this transfer and any/all sub-transfers as cancelled
                    megaTransfer->getCancelToken()->cancel();

                    // report cancellation signalled
                    fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(API_OK));
                    return API_OK;
                }
            }

            if (!megaTransfer->isStreamingTransfer())
            {
                Transfer *transfer = megaTransfer->getTransfer();
                if (!transfer)
                {
                    return API_ENOENT;
                }
                #ifdef _WIN32
                    if (transfer->type == GET)
                    {
                        RemoveHiddenFileAttribute(transfer->localfilename);
                    }
                #endif

                MegaErrorPrivate megaError(API_EINCOMPLETE);
                megaTransfer->setLastError(&megaError);

                bool found = false;
                file_list files = transfer->files;
                file_list::iterator iterator = files.begin();
                while (iterator != files.end())
                {
                    File *file = *iterator;
                    iterator++;
                    if (file->tag == transferTag)
                    {
                        found = true;
                        if (!file->syncxfer)
                        {
                            client->stopxfer(file, &committer);
                            fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(API_OK));
                        }
                        else
                        {
                            fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(API_EACCESS));
                        }
                        break;
                    }
                }

                if (!found)
                {
                    fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(API_ENOENT));
                }
            }
            else
            {
                m_off_t startPos = megaTransfer->getStartPos();
                m_off_t endPos = megaTransfer->getEndPos();
                m_off_t totalBytes = endPos - startPos + 1;

                MegaNode *publicNode = megaTransfer->getPublicNode();
                if (publicNode)
                {
                    client->preadabort(publicNode->getHandle(), startPos, totalBytes);
                }
                else
                {
                    std::shared_ptr<Node> node = client->nodebyhandle(megaTransfer->getNodeHandle());
                    if (node)
                    {
                        client->preadabort(node.get(), startPos, totalBytes);
                    }
                }
                fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(API_OK));
            }
            return API_OK;
}

void MegaApiImpl::cancelTransfers(int direction, MegaRequestListener* listener)
{
    MegaRequestPrivate* request = new MegaRequestPrivate(MegaRequest::TYPE_CANCEL_TRANSFERS, listener);
    request->setParamType(direction);

    request->performRequest = [this, request]()
        {
            int direction = request->getParamType();

            if ((direction != MegaTransfer::TYPE_DOWNLOAD) && (direction != MegaTransfer::TYPE_UPLOAD))
            {
                return API_EARGS;
            }

            CancelToken cancelled(true);

            // 1. Set all intermediate layer MegaTransfer object cancel tokens
            transferQueue.setAllCancelled(cancelled, direction);
            for (auto& t : transferMap)
            {
                if (t.second->getType() == direction
                   && !t.second->isSyncTransfer()
                   && !t.second->isStreamingTransfer())
                {
                    t.second->setCancelToken(cancelled);
                }
            }

            // 2. cancel transfers that were already startxfer'd
            for (auto& it : client->multi_transfers[direction])
            {
                for (auto& it2 : it.second->files)
                {
                    if (!it2->syncxfer)
                    {
                        it2->cancelToken = cancelled;
                    }
                }
            }

            LOG_verbose << "Marked all non-sync non-streaming transfers as cancelled. direction: " << direction;
            fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(API_OK));
            return API_OK;
        };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::setScheduledCopy(const char* localFolder, MegaNode* parent, bool attendPastBackups, int64_t period, string periodstring, int numBackups, MegaRequestListener* listener)
{
    MegaRequestPrivate* request = new MegaRequestPrivate(MegaRequest::TYPE_ADD_SCHEDULED_COPY, listener);
    if (parent) request->setNodeHandle(parent->getHandle());
    if (localFolder)
    {
        request->setFile(localFolder);
    }

    request->setNumRetry(numBackups);
    request->setNumber(period);
    request->setText(periodstring.c_str());
    request->setFlag(attendPastBackups);

    request->performRequest = [this, request]()
        {
            std::shared_ptr<Node> parent = client->nodebyhandle(request->getNodeHandle());
            const char *localPath = request->getFile();
            if(!parent || (parent->type==FILENODE) || !localPath)
            {
                return API_EARGS;
            }

            string utf8name(localPath);
            MegaScheduledCopyController *mbc = NULL;
            int tagexisting = 0;
            bool existing = false;
            for (std::map<int, MegaScheduledCopyController *>::iterator it = backupsMap.begin(); it != backupsMap.end(); ++it)
            {
                if (!strcmp(it->second->getLocalFolder(), utf8name.c_str()) && it->second->getMegaHandle() == request->getNodeHandle())
                {
                    existing = true;
                    mbc = it->second;
                    tagexisting = it->first;
                }
            }

            if (existing){
                LOG_debug << "Updating existing backup parameters: " <<  utf8name.c_str() << " to " << request->getNodeHandle();
                mbc->setPeriod(request->getNumber());
                mbc->setPeriodstring(request->getText());
                mbc->setMaxBackups(request->getNumRetry());
                mbc->setAttendPastBackups(request->getFlag());

                request->setTransferTag(tagexisting);
                if (!mbc->isValid())
                {
                    LOG_err << "Failed to update backup parameters: Invalid parameters";
                    return API_EARGS;
                }
            }
            else
            {
                int tag = request->getTag();
                int tagForFolderTansferTag = client->nextreqtag();
                string speriod = request->getText();
                bool attendPastBackups= request->getFlag();
                //TODO: add existence of local folder check (optional??)

                MegaScheduledCopyController* newScheduledCopyController =
                    new MegaScheduledCopyController(this,
                                                    tag,
                                                    tagForFolderTansferTag,
                                                    request->getNodeHandle(),
                                                    utf8name.c_str(),
                                                    attendPastBackups,
                                                    speriod.c_str(),
                                                    request->getNumber(),
                                                    request->getNumRetry());
                // TODO: should we add this in setScheduledCopy?
                newScheduledCopyController->setBackupListener(request->getBackupListener());
                if (newScheduledCopyController->isValid())
                {
                    backupsMap[tag] = newScheduledCopyController;
                    request->setTransferTag(tag);
                }
                else
                {
                    delete newScheduledCopyController;
                    return API_EARGS;
                }
            }

            fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(API_OK));

            return API_OK;
        };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::removeScheduledCopy(int tag, MegaRequestListener* listener)
{
    MegaRequestPrivate* request = new MegaRequestPrivate(MegaRequest::TYPE_REMOVE_SCHEDULED_COPY, listener);
    request->setNumber(tag);

    request->performRequest = [this, request]()
        {
            int backuptag = int(request->getNumber());
            bool found = false;
            bool flag = request->getFlag();
            error e   = API_OK;

            map<int, MegaScheduledCopyController *>::iterator itr = backupsMap.find(backuptag) ;
            if (itr != backupsMap.end())
            {
                found = true;
            }

            if (found)
            {
                if (!flag)
                {
                    MegaRequestPrivate *requestabort = new MegaRequestPrivate(MegaRequest::TYPE_ABORT_CURRENT_SCHEDULED_COPY);
                    requestabort->setNumber(backuptag);
                    requestabort->performRequest = [this, requestabort]()
                    {
                        return processAbortBackupRequest(requestabort);
                    };

                    int nextTag = client->nextreqtag();
                    requestabort->setTag(nextTag);
                    requestMap[nextTag]=requestabort;
                    fireOnRequestStart(requestabort);

                    e = processAbortBackupRequest(requestabort);
                    if (e)
                    {
                        LOG_err << "Failed to abort backup upon remove request";
                        fireOnRequestFinish(requestabort, std::make_unique<MegaErrorPrivate>(API_OK));
                    }
                    else
                    {
                        request->setFlag(true);
                        requestQueue.push(request);
                    }
                }
                else
                {
                    MegaScheduledCopyController * todelete = itr->second;
                    backupsMap.erase(backuptag);
                    fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(API_OK));
                    delete todelete;
                }
            }
            else
            {
                e = API_ENOENT;
            }

            return e;
        };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::startTimer(int64_t period, MegaRequestListener* listener)
{
    MegaRequestPrivate* request = new MegaRequestPrivate(MegaRequest::TYPE_TIMER, listener);
    request->setNumber(period);

    request->performRequest = [this, request]()
        {
            int delta = int(request->getNumber());
            TimerWithBackoff *twb = new TimerWithBackoff(client->rng, request->getTag());
            twb->backoff(delta);
            return client->addtimer(twb);
        };

    requestQueue.push(request);
    waiter->notify();
}

#ifdef ENABLE_SYNC
void MegaApiImpl::loadExternalBackupSyncsFromExternalDrive(const char* externalDriveRoot, MegaRequestListener* listener)
{
    MegaRequestPrivate* request = new MegaRequestPrivate(MegaRequest::TYPE_LOAD_EXTERNAL_DRIVE_BACKUPS, listener);
    request->setFile(externalDriveRoot);
    request->setListener(listener);

    request->performRequest = [this, request]()
        {
            const char* externalDrive = request->getFile();
            if (!externalDrive)
            {
                return API_EARGS;
            }
            else
            {
                client->syncs.backupOpenDrive(LocalPath::fromAbsolutePath(externalDrive), [this, request](Error e){
                    fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e), true);
                });
                return API_OK;
            }
        };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::closeExternalBackupSyncsFromExternalDrive(const char* externalDriveRoot, MegaRequestListener* listener)
{
    MegaRequestPrivate* request = new MegaRequestPrivate(MegaRequest::TYPE_CLOSE_EXTERNAL_DRIVE_BACKUPS, listener);
    request->setFile(externalDriveRoot);
    request->setListener(listener);

    request->performRequest = [this, request]()
        {
            const char* externalDrive = request->getFile();
            if (!externalDrive)
            {
                return API_EARGS;
            }
            else
            {
                client->syncs.backupCloseDrive(LocalPath::fromAbsolutePath(externalDrive), [this, request](Error e){
                    fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e), true);
                });
                return API_OK;
            }
        };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::importSyncConfigs(const char* configs, MegaRequestListener* listener)
{
    MegaRequestPrivate* request = new MegaRequestPrivate(MegaRequest::TYPE_IMPORT_SYNC_CONFIGS, listener);
    request->setText(configs);

    request->performRequest = [this, request]()
        {
            if (const auto* configs = request->getText())
            {
                auto completion =
                  [request, this](error result)
                  {
                      auto error = std::make_unique<MegaErrorPrivate>(result);
                      fireOnRequestFinish(request, std::move(error));
                  };

                client->importSyncConfigs(configs, std::move(completion));
                return API_OK;
            }

            return API_EARGS;
        };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::copySyncDataToCache(const char*,
                                      const char*,
                                      MegaHandle,
                                      const char*,
                                      long long,
                                      bool,
                                      bool,
                                      MegaRequestListener* listener)
{
    MegaRequestPrivate* request = new MegaRequestPrivate(MegaRequest::TYPE_COPY_SYNC_CONFIG, listener);
    request->performRequest = []()
    {
        return API_EINTERNAL;
    };
    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::copyCachedStatus(int, int, int, MegaRequestListener* listener)
{
    MegaRequestPrivate* request = new MegaRequestPrivate(MegaRequest::TYPE_COPY_CACHED_STATUS, listener);
    request->performRequest = []()
    {
        return API_EINTERNAL;
    };
    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::removeSyncById(handle backupId, MegaRequestListener* listener)
{
    MegaRequestPrivate* request = new MegaRequestPrivate(MegaRequest::TYPE_REMOVE_SYNC, listener);
    request->setParentHandle(backupId);
    request->setFlag(true);

    request->performRequest = [this, request]()
        {
            auto backupId = request->getParentHandle();
            if (backupId == INVALID_HANDLE)
            {
                return API_EARGS;
            }

            SyncConfig c;
            if (!client->syncs.syncConfigByBackupId(backupId, c))
            {
                LOG_err << "Backup id not found: " << Base64Str<MegaClient::BACKUPHANDLE>(backupId);
                return API_ENOENT;
            }

            request->setFile(c.mLocalPath.toPath(false).c_str());

            std::function<void(Error)> completion = [request, this](Error e) {
                fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(error(e)));
            };

            client->syncs.deregisterThenRemoveSyncById(backupId, std::move(completion));
            return API_OK;
        };

    requestQueue.push(request);
    waiter->notify();
}
#endif  // ENABLE_SYNC

void MegaApiImpl::reportEvent(const char* details, MegaRequestListener* listener)
{
    MegaRequestPrivate* request = new MegaRequestPrivate(MegaRequest::TYPE_REPORT_EVENT, listener);
    request->setText(details);

    request->performRequest = [this, request]()
        {
            const char *details = request->getText();
            if(!details)
            {
                return API_EARGS;
            }

            string event = "A"; //Application event
            size_t size = strlen(details);
            char* base64details = new char[size * 4 / 3 + 4];
            Base64::btoa((byte *)details, size, base64details);
            client->reportevent(event.c_str(), base64details);
            delete [] base64details;
            return API_OK;
        };

    requestQueue.push(request);
    waiter->notify();
}

error MegaApiImpl::performRequest_enumeratequotaitems(MegaRequestPrivate* request)
{
            if ((request->getType() == MegaRequest::TYPE_GET_PAYMENT_ID)
                && (request->getParamType() < mega::MegaApi::AFFILIATE_TYPE_INVALID
                    || request->getParamType() > mega::MegaApi::AFFILIATE_TYPE_CONTACT))
            {
                return API_EARGS;
            }

            int method = int(request->getNumber());
            if(method != MegaApi::PAYMENT_METHOD_BALANCE && method != MegaApi::PAYMENT_METHOD_CREDIT_CARD)
            {
                return API_EARGS;
            }

            std::optional<std::string> countryCode;
            if (request->getText())
            {
                countryCode = request->getText();
            }
            client->purchase_enumeratequotaitems(countryCode);
            return API_OK;
}

void MegaApiImpl::submitPurchaseReceipt(int gateway, const char* receipt, MegaHandle lastPublicHandle, int lastPublicHandleType, int64_t lastAccessTimestamp, MegaRequestListener* listener)
{
    MegaRequestPrivate* request = new MegaRequestPrivate(MegaRequest::TYPE_SUBMIT_PURCHASE_RECEIPT, listener);
    request->setNumber(gateway);
    request->setText(receipt);
    request->setNodeHandle(lastPublicHandle);
    request->setParamType(lastPublicHandleType);
    request->setTransferredBytes(lastAccessTimestamp);

    request->performRequest = [this, request]()
        {
            const char* receipt = request->getText();
            int type = int(request->getNumber());
            handle lph = request->getNodeHandle();
            int phtype = request->getParamType();
            int64_t ts = request->getTransferredBytes();

            if (request->getParamType() < mega::MegaApi::AFFILIATE_TYPE_INVALID
                || request->getParamType() > mega::MegaApi::AFFILIATE_TYPE_CONTACT)
            {
                return API_EARGS;
            }

            if(!receipt || (type != MegaApi::PAYMENT_METHOD_GOOGLE_WALLET
                            && type != MegaApi::PAYMENT_METHOD_ITUNES
                            && type != MegaApi::PAYMENT_METHOD_WINDOWS_STORE && type != MegaApi::PAYMENT_METHOD_HUAWEI_WALLET))
            {
                return API_EARGS;
            }

            if(type == MegaApi::PAYMENT_METHOD_ITUNES && client->loggedin() != FULLACCOUNT)
            {
                return API_EACCESS;
            }

            string base64receipt;
            if (type == MegaApi::PAYMENT_METHOD_GOOGLE_WALLET
                    || type == MegaApi::PAYMENT_METHOD_WINDOWS_STORE || type == MegaApi::PAYMENT_METHOD_HUAWEI_WALLET)
            {
                size_t len = strlen(receipt);
                base64receipt.resize(len * 4 / 3 + 4);
                base64receipt.resize(
                    Base64::btoa((byte*)receipt, len, (char*)base64receipt.data()));
            }
            else // MegaApi::PAYMENT_METHOD_ITUNES
            {
                base64receipt = receipt;
            }

            client->submitpurchasereceipt(type, base64receipt.c_str(), lph, phtype, ts);
            return API_OK;
        };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::creditCardStore(const char* address1, const char* address2, const char* city,
    const char* province, const char* country, const char* postalcode,
    const char* firstname, const char* lastname, const char* creditcard,
    const char* expire_month, const char* expire_year, const char* cv2,
    MegaRequestListener* listener)
{
    MegaRequestPrivate* request = new MegaRequestPrivate(MegaRequest::TYPE_CREDIT_CARD_STORE, listener);
    string email;

    {
        SdkMutexGuard g(sdkMutex);
        User* u = client->finduser(client->me);
        if (u)
        {
            email = u->email;
        }
    }

    if (email.size())
    {
        string saddress1, saddress2, scity, sprovince, scountry, spostalcode;
        string sfirstname, slastname, screditcard, sexpire_month, sexpire_year, scv2;

        if (address1)
        {
            saddress1 = address1;
        }

        if (address2)
        {
            saddress2 = address2;
        }

        if (city)
        {
            scity = city;
        }

        if (province)
        {
            sprovince = province;
        }

        if (country)
        {
            scountry = country;
        }

        if (postalcode)
        {
            spostalcode = postalcode;
        }

        if (firstname)
        {
            sfirstname = firstname;
        }

        if (lastname)
        {
            slastname = lastname;
        }

        if (creditcard)
        {
            screditcard = creditcard;
            screditcard.erase(std::remove_if(screditcard.begin(), screditcard.end(), char_is_not_digit), screditcard.end());
        }

        if (expire_month)
        {
            sexpire_month = expire_month;
        }

        if (expire_year)
        {
            sexpire_year = expire_year;
        }

        if (cv2)
        {
            scv2 = cv2;
        }

        size_t tam = 256 + sfirstname.size() + slastname.size() + screditcard.size() +
                     sexpire_month.size() + sexpire_year.size() + scv2.size() + saddress1.size() +
                     saddress2.size() + scity.size() + sprovince.size() + spostalcode.size() +
                     scountry.size() + email.size();

        char* ccplain = new char[tam];
        snprintf(ccplain, tam, "{\"first_name\":\"%s\",\"last_name\":\"%s\","
            "\"card_number\":\"%s\","
            "\"expiry_date_month\":\"%s\",\"expiry_date_year\":\"%s\","
            "\"cv2\":\"%s\",\"address1\":\"%s\","
            "\"address2\":\"%s\",\"city\":\"%s\","
            "\"province\":\"%s\",\"postal_code\":\"%s\","
            "\"country_code\":\"%s\",\"email_address\":\"%s\"}", sfirstname.c_str(), slastname.c_str(),
            screditcard.c_str(), sexpire_month.c_str(), sexpire_year.c_str(), scv2.c_str(), saddress1.c_str(),
            saddress2.c_str(), scity.c_str(), sprovince.c_str(), spostalcode.c_str(), scountry.c_str(), email.c_str());

        request->setText((const char*)ccplain);
        delete[] ccplain;
    }

    request->performRequest = [this, request]()
        {
            const char* ccplain = request->getText();
            return client->creditcardstore(ccplain);
        };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::creditCardQuerySubscriptions(MegaRequestListener* listener)
{
    MegaRequestPrivate* request = new MegaRequestPrivate(MegaRequest::TYPE_CREDIT_CARD_QUERY_SUBSCRIPTIONS, listener);

    request->performRequest = [this]()
        {
            client->creditcardquerysubscriptions();
            return API_OK;
        };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::creditCardCancelSubscriptions(const char* reason,
                                                const char* id,
                                                int canContact,
                                                MegaRequestListener* listener)
{
    MegaRequestPrivate* request = new MegaRequestPrivate(MegaRequest::TYPE_CREDIT_CARD_CANCEL_SUBSCRIPTIONS, listener);
    request->setText(reason);
    request->setName(id);
    request->setNumDetails(canContact);

    request->performRequest = [this, request]()
    {
        CommandCreditCardCancelSubscriptions::CancelSubscription cancelSubscription{
            request->getText(), // reason
            request->getName(), // id
            request->getNumDetails(), // canContact
        };
        client->creditcardcancelsubscriptions(cancelSubscription);
        return API_OK;
    };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::creditCardCancelSubscriptions(const MegaCancelSubscriptionReasonList* reasons,
                                                const char* id,
                                                int canContact,
                                                MegaRequestListener* listener)
{
    MegaRequestPrivate* request =
        new MegaRequestPrivate(MegaRequest::TYPE_CREDIT_CARD_CANCEL_SUBSCRIPTIONS, listener);
    request->setMegaCancelSubscriptionReasons(reasons ? reasons->copy() : nullptr);
    request->setName(id);
    request->setNumDetails(canContact);

    request->performRequest = [this, request]()
    {
        vector<pair<string, string>> reasons;
        if (const MegaCancelSubscriptionReasonList* megaReasons =
                request->getMegaCancelSubscriptionReasons())
        {
            for (size_t i = 0; i < megaReasons->size(); ++i)
            {
                const MegaCancelSubscriptionReason* r = megaReasons->get(i);
                assert(r && r->text() && r->position());
                string text{(r && r->text()) ? r->text() : ""};
                string position{(r && r->position()) ? r->position() : ""};
                reasons.emplace_back(std::move(text), std::move(position));
            }
        }
        CommandCreditCardCancelSubscriptions::CancelSubscription cancelSubscription{
            std::move(reasons),
            request->getName(), // id
            request->getNumDetails(), // canContact
        };
        client->creditcardcancelsubscriptions(cancelSubscription);
        return API_OK;
    };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::getPaymentMethods(MegaRequestListener* listener)
{
    MegaRequestPrivate* request = new MegaRequestPrivate(MegaRequest::TYPE_GET_PAYMENT_METHODS, listener);

    request->performRequest = [this]()
        {
            client->getpaymentmethods();
            return API_OK;
        };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::submitFeedback(int rating,
                                 const char* comment,
                                 bool transferFeedback,
                                 int transferType,
                                 MegaRequestListener* listener)
{
    MegaRequestPrivate* request =
        new MegaRequestPrivate(MegaRequest::TYPE_SUBMIT_FEEDBACK, listener);
    request->setText(comment);
    request->setNumber(rating);
    request->setFlag(transferFeedback);
    request->setParamType(transferType);

    request->performRequest = [this, request]()
    {
        const int rating{static_cast<int>(request->getNumber())};
        if (rating < 1 || rating > 5)
        {
            LOG_err << "Request (TYPE_SUBMIT_FEEDBACK). Invalid rating: " << rating;
            return API_EARGS;
        }

        const int transferType{request->getParamType()};
        if (transferType < MegaApi::TRANSFER_STATS_DOWNLOAD ||
            transferType > MegaApi::TRANSFER_STATS_MAX)
        {
            LOG_err << "Request (TYPE_SUBMIT_FEEDBACK). Invalid transferType: " << transferType;
            return API_EARGS;
        }

        const bool transferFeedback{request->getFlag()};
        const char* comment{request->getText()};
        sendUserfeedback(rating, comment, transferFeedback, transferType);
        return API_OK;
    };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::sendEvent(int eventType, const char* message, bool addJourneyId, const char* viewId, MegaRequestListener* listener)
{
    MegaRequestPrivate* request = new MegaRequestPrivate(MegaRequest::TYPE_SEND_EVENT, listener);
    request->setNumber(eventType);
    request->setText(message);
    request->setFlag(addJourneyId);
    request->setSessionKey(viewId);

    request->performRequest = [this, request]()
        {
            // See https://confluence.developers.mega.co.nz/display/STS/Event+Ranges
            static const std::vector<std::pair<int, int>> excluded = {
                {     0,   98000}, // Below MEGAproxy.
                { 99600,  100000}, // WebClient range.
                {100000,  300000}, // API extended range.
                {500000,  600000}, // WebClient extended range.
                {750000,  800000}, // API extended range.
                {900000, INT_MAX}  // Unassigned.
            }; // excluded

            const char *text = request->getText();

            // No message.
            if (!text)
                return API_EARGS;

            int number = int(request->getNumber());

            // Is the event in an acceptable range?
            for (auto& e : excluded)
            {
                // Events in a disallowed range.
                if (number >= e.first && number < e.second)
                    return API_EARGS;
            }

            const char* viewId = request->getSessionKey();
            bool addJourneyId = request->getFlag();
            client->sendevent(number, text, viewId, addJourneyId);
            return API_OK;
        };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::createSupportTicket(const char* message, int type, MegaRequestListener* listener)
{
    MegaRequestPrivate* request = new MegaRequestPrivate(MegaRequest::TYPE_SUPPORT_TICKET, listener);
    request->setParamType(type);
    request->setText(message);

    request->performRequest = [this, request]()
        {
            int type = request->getParamType();
            const char *message = request->getText();

            if ((type < 0 || type > 10) || !message)
            {
                return API_EARGS;
            }

            client->supportticket(message, type);
            return API_OK;
        };

    requestQueue.push(request);
    waiter->notify();
}

error MegaApiImpl::performRequest_getUserData(MegaRequestPrivate* request)
{
            const char* email = request->getEmail();
            if (request->getFlag() && !email)
            {
                return API_EARGS;
            }

            if(!request->getFlag())
            {
                client->getuserdata(client->reqtag);
            }
            else
            {
                client->getpubkey(email);
            }

            return API_OK;
}

void MegaApiImpl::sendDevCommand(const char* command, const char* email, long long quota, int businessStatus, int userStatus, MegaRequestListener* listener)
{
    MegaRequestPrivate* request = new MegaRequestPrivate(MegaRequest::TYPE_SEND_DEV_COMMAND, listener);
    request->setName(command);
    request->setEmail(email);
    request->setTotalBytes(quota);
    request->setAccess(businessStatus);
    request->setNumDetails(userStatus);

    request->performRequest = [this, request]()
        {
            const char *command = request->getName();
            if (!command)
            {
                return API_EARGS;
            }

            const char* email = request->getEmail();
            long long q = request->getTotalBytes();
            int bs = request->getAccess();
            int us = request->getNumDetails();

            bool isSalSubcmd = !strcmp(command, "sal");
            bool isOdqSubcmd = !strcmp(command, "aodq");
            bool isTqSubcmd = !strcmp(command, "tq");
            bool isBsSubcmd = !strcmp(command, "bs");
            bool isUsSubcmd = !strcmp(command, "us");
            bool isFrSubcmd = !strcmp(command, "fr");   // force reload via -6 on sc channel

            if (!isOdqSubcmd && !isTqSubcmd && !isBsSubcmd && !isUsSubcmd && !isFrSubcmd &&
                !isSalSubcmd)
            {
                return API_EARGS;
            }

            if (isTqSubcmd)
            {
                if (q < 0)
                {
                    return API_EARGS;
                }
            }
            else if (isBsSubcmd)
            {
                if (bs < -1 || bs > 2)
                {
                    return API_EARGS;
                }
            }
            else if (isUsSubcmd)
            {
                if (us == 1 || us < 0 || us > 9)
                {
                    return API_EARGS;
                }
            }
            client->senddevcommand(command, email, q, bs, us);
            return API_OK;
        };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::killSession(MegaHandle sessionHandle, MegaRequestListener* listener)
{
    MegaRequestPrivate* request = new MegaRequestPrivate(MegaRequest::TYPE_KILL_SESSION, listener);
    request->setNodeHandle(sessionHandle);

    request->performRequest = [this, request]()
        {
            MegaHandle handle = request->getNodeHandle();
            if (handle == INVALID_HANDLE)
            {
                client->killallsessions();
            }
            else
            {
                client->killsession(handle);
            }
            return API_OK;
        };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::getSessionTransferURL(const char* path, MegaRequestListener* listener)
{
    MegaRequestPrivate* request = new MegaRequestPrivate(MegaRequest::TYPE_GET_SESSION_TRANSFER_URL, listener);
    request->setText(path);

    request->performRequest = [this, request]()
        {
            error e = client->copysession();

            if (e == API_ENOENT)    // no session to copy because not logged in
            {
                string url = MegaClient::getMegaURL() + "/#";
                auto path = request->getText();
                if (path) url.append(path);
                request->setLink(url.c_str());

                e = API_OK;
                fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e));
                return e;
            }
            return e;
        };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::resendVerificationEmail(MegaRequestListener* listener)
{
    MegaRequestPrivate* request = new MegaRequestPrivate(MegaRequest::TYPE_RESEND_VERIFICATION_EMAIL, listener);

    request->performRequest = [this]()
        {
            client->resendverificationemail();
            return API_OK;
        };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::resetSmsVerifiedPhoneNumber(MegaRequestListener* listener)
{
    MegaRequestPrivate* request = new MegaRequestPrivate(MegaRequest::TYPE_RESET_SMS_VERIFIED_NUMBER, listener);

    request->performRequest = [this]()
        {
            client->resetSmsVerifiedPhoneNumber();
            return API_OK;
        };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::cleanRubbishBin(MegaRequestListener* listener)
{
    MegaRequestPrivate* request = new MegaRequestPrivate(MegaRequest::TYPE_CLEAN_RUBBISH_BIN, listener);

    request->performRequest = [this]()
        {
            client->cleanrubbishbin();
            return API_OK;
        };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::setProxySettings(MegaProxy* proxySettings, MegaRequestListener* listener)
{
    Proxy* localProxySettings = new Proxy();
    localProxySettings->setProxyType(proxySettings->getProxyType());

    string url;
    if (proxySettings->getProxyURL())
        url = proxySettings->getProxyURL();

    string localurl;

#if defined(_WIN32)
    localurl = url;
#else
    LocalPath::path2local(&url, &localurl);
#endif

    localProxySettings->setProxyURL(localurl);

    if (proxySettings->credentialsNeeded())
    {
        string username;
        if (proxySettings->getUsername())
            username = proxySettings->getUsername();

        string localusername;

#if defined(_WIN32)
        localusername = username;
#else
        LocalPath::path2local(&username, &localusername);
#endif

        string password;
        if (proxySettings->getPassword())
            password = proxySettings->getPassword();

        string localpassword;

#if defined(_WIN32)
        localpassword = password;
#else
        LocalPath::path2local(&password, &localpassword);
#endif

        localProxySettings->setCredentials(localusername, localpassword);
    }

    MegaRequestPrivate* request = new MegaRequestPrivate(MegaRequest::TYPE_SET_PROXY, listener);
    request->setProxy(localProxySettings);

    request->performRequest = [this, request]()
    {
        auto proxy = makeUniqueFrom(request->getProxy());
        httpio->setproxy(*proxy);
        fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(API_OK));
        return API_OK;
    };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::getLastAvailableVersion(const char* /*anyAppKey*/, MegaRequestListener* listener)
{
    MegaRequestPrivate* request = new MegaRequestPrivate(MegaRequest::TYPE_APP_VERSION, listener);

    request->performRequest = []()
    {
        return API_EACCESS;
    };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::getLocalSSLCertificate(MegaRequestListener* listener)
{
    MegaRequestPrivate* request = new MegaRequestPrivate(MegaRequest::TYPE_GET_LOCAL_SSL_CERT, listener);

    request->performRequest = [this]()
        {
            client->getlocalsslcertificate();
            return API_OK;
        };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::queryDNS(const char* hostname, MegaRequestListener* listener)
{
    MegaRequestPrivate* request = new MegaRequestPrivate(MegaRequest::TYPE_QUERY_DNS, listener);
    request->setName(hostname);

    request->performRequest = [this, request]()
        {
            const char *hostname = request->getName();
            if (!hostname)
            {
                return API_EARGS;
            }

            client->dnsrequest(hostname);
            return API_OK;
        };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::downloadFile(const char* url, const char* dstpath, MegaRequestListener* listener)
{
    MegaRequestPrivate* request = new MegaRequestPrivate(MegaRequest::TYPE_DOWNLOAD_FILE, listener);
    request->setLink(url);
    request->setFile(dstpath);

    request->performRequest = [this, request]()
        {
            const char *url = request->getLink();
            const char *file = request->getFile();
            if (!url || !file)
            {
                return API_EARGS;
            }

            client->httprequest(url, METHOD_GET, true);
            return API_OK;
        };

    requestQueue.push(request);
    waiter->notify();
}

#ifdef ENABLE_CHAT
void MegaApiImpl::createChat(bool group, bool publicchat, MegaTextChatPeerList* peers, const MegaStringMap* userKeyMap, const char* title, bool meetingRoom, int chatOptions, const MegaScheduledMeeting* scheduledMeeting, MegaRequestListener* listener)
{
    MegaRequestPrivate* request = new MegaRequestPrivate(MegaRequest::TYPE_CHAT_CREATE, listener);
    request->setFlag(group);
    request->setAccess(publicchat ? 1 : 0);
    request->setMegaTextChatPeerList(peers);
    request->setText(title);
    request->setMegaStringMap(userKeyMap);
    request->setNumber(meetingRoom);
    request->setParamType(chatOptions);
    if (scheduledMeeting)
    {
        std::unique_ptr<MegaScheduledMeetingList> l(MegaScheduledMeetingList::createInstance());
        l->insert(scheduledMeeting->copy());
        request->setMegaScheduledMeetingList(l.get());
    }

    request->performRequest = [this, request]()
        {
            MegaTextChatPeerList *chatPeers = request->getMegaTextChatPeerList();
            bool group = request->getFlag();
            const char *title = request->getText();
            bool publicchat = (request->getAccess() == 1);
            MegaStringMap* userKeyMap = request->getMegaStringMap();
            int numPeers = chatPeers ? chatPeers->size() : 0;
            const string_map *uhkeymap = NULL;
            if(publicchat)
            {
                if (!group || !userKeyMap
                        || (userKeyMap->size() != numPeers + 1))    // includes our own key
                {
                    return API_EARGS;
                }
                uhkeymap = ((MegaStringMapPrivate*)userKeyMap)->getMap();
            }
            else
            {
                if (!group && numPeers > 1)
                {
                    return API_EACCESS;
                }
            }

            bool meetingRoom = static_cast<bool>(request->getNumber());
            if (!group && request->getMegaStringList())
            {
                return API_EARGS;
            }

            MegaScheduledMeetingPrivate* schedMeeting = nullptr;
            if (request->getMegaScheduledMeetingList() && request->getMegaScheduledMeetingList()->size() == 1)
            {
                schedMeeting = static_cast<MegaScheduledMeetingPrivate*>(request->getMegaScheduledMeetingList()->at(0));
            }

            const userpriv_vector* userpriv;
            if (numPeers)
            {
                userpriv = ((MegaTextChatPeerListPrivate*)chatPeers)->getList();
                // if 1:1 chat, peer is enforced to be moderator too
                if (!group && numPeers && userpriv->at(0).second != PRIV_MODERATOR)
                {
                    ((MegaTextChatPeerListPrivate*)chatPeers)
                        ->setPeerPrivilege(userpriv->at(0).first, PRIV_MODERATOR);
                }
            }
            else
            {
                userpriv = nullptr;
            }
            client->createChat(group, publicchat, userpriv, uhkeymap, title, meetingRoom, request->getParamType() /*chat options value*/, schedMeeting ? schedMeeting->scheduledMeeting() : nullptr);
            return API_OK;
        };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::setChatOption(MegaHandle chatid, int option, bool enabled, MegaRequestListener* listener)
{
    MegaRequestPrivate* request = new MegaRequestPrivate(MegaRequest::TYPE_SET_CHAT_OPTIONS, listener);
    request->setNodeHandle(chatid);
    request->setAccess(option);
    request->setFlag(enabled);

    request->performRequest = [this, request]()
        {
            handle chatid = request->getNodeHandle();
            if (chatid == INVALID_HANDLE)
            {
                return API_EARGS;
            }

            textchat_map::iterator it = client->chats.find(chatid);
            if (it == client->chats.end())
            {
                return API_ENOENT;
            }

            TextChat* chat = it->second;
            if (!chat->getGroup())
            {
                return API_EARGS;
            }

            client->queueCommand(new CommandSetChatOptions(
                client,
                chatid,
                request->getAccess() /*option*/,
                request->getFlag() /*enabled*/,
                [request, this](Error e)
                {
                    fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e));
                }));
            return API_OK;
        };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::inviteToChat(MegaHandle chatid, MegaHandle uh, int privilege, bool openMode, const char* unifiedKey, const char* title, MegaRequestListener* listener)
{
    MegaRequestPrivate* request = new MegaRequestPrivate(MegaRequest::TYPE_CHAT_INVITE, listener);
    request->setNodeHandle(chatid);
    request->setParentHandle(uh);
    request->setAccess(privilege);
    request->setText(title);
    request->setFlag(openMode);
    request->setSessionKey(unifiedKey);

    request->performRequest = [this, request]()
        {
            handle chatid = request->getNodeHandle();
            handle uh = request->getParentHandle();
            int access = request->getAccess();
            const char *title = request->getText();
            bool publicMode = request->getFlag();
            const char *unifiedKey = request->getSessionKey();

            if (publicMode && !unifiedKey)
            {
                LOG_err << "Request (TYPE_CHAT_INVITE). Invalid unified key for Public chat: " << Base64Str<MegaClient::USERHANDLE>(chatid);
                return API_EINCOMPLETE;
            }

            if (chatid == INVALID_HANDLE || uh == INVALID_HANDLE)
            {
                LOG_err << "Request (TYPE_CHAT_INVITE). Invalid user handle: " << Base64Str<MegaClient::USERHANDLE>(uh)
                << ", or chatid: " << Base64Str<MegaClient::USERHANDLE>(chatid);
                return API_ENOENT;
            }

            textchat_map::iterator it = client->chats.find(chatid);
            if (it == client->chats.end())
            {
                LOG_err << "Request (TYPE_CHAT_INVITE). chatroom not found: " << Base64Str<MegaClient::USERHANDLE>(chatid);
                return API_ENOENT;
            }

            TextChat *chat = it->second;
            if (chat->publicChat() != publicMode)
            {
                LOG_err << "Request (TYPE_CHAT_INVITE). " << (chat->publicChat() ? "Public chat mode" : "Private chat mode")
                << " ,unexpected for chat: " << Base64Str<MegaClient::USERHANDLE>(chatid);
                return API_EACCESS;
            }

            // new participants of private chats require the title to be encrypted to them
            if (!chat->publicChat() && (!chat->getTitle().empty() && (!title || title[0] == '\0')))
            {
                LOG_err << "Request (TYPE_CHAT_INVITE). Invalid title for chat: " << Base64Str<MegaClient::USERHANDLE>(chatid);
                return API_EINCOMPLETE;
            }

            if (!chat->getGroup())
            {
                LOG_err << "Request (TYPE_CHAT_INVITE). Invalid chat (1on1): " << Base64Str<MegaClient::USERHANDLE>(chatid);
                return API_EACCESS;
            }

            if (chat->getOwnPrivileges() < PRIV_MODERATOR)
            {
                ChatOptions chatOptions(static_cast<ChatOptions_t>(chat->getChatOptions()));
                if (chat->getOwnPrivileges() < PRIV_STANDARD || !chatOptions.openInvite())
                {
                    // only allowed moderators or participants with standard permissions just if openInvite is enabled
                    LOG_err << "Request (TYPE_CHAT_INVITE). Insufficient permissions to perform this action, for chat: "
                    << Base64Str<MegaClient::USERHANDLE>(chatid);
                    return API_EACCESS;
                }
            }

            client->inviteToChat(chatid, uh, access, unifiedKey, title);
            return API_OK;
        };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::removeFromChat(MegaHandle chatid, MegaHandle uh, MegaRequestListener* listener)
{
    MegaRequestPrivate* request = new MegaRequestPrivate(MegaRequest::TYPE_CHAT_REMOVE, listener);
    request->setNodeHandle(chatid);
    if (uh != INVALID_HANDLE)   // if not provided, it removes oneself from the chat
    {
        request->setParentHandle(uh);
    }

    request->performRequest = [this, request]()
        {
            handle chatid = request->getNodeHandle();
            handle uh = request->getParentHandle();

            if (chatid == INVALID_HANDLE)
            {
                return API_ENOENT;
            }

            textchat_map::iterator it = client->chats.find(chatid);
            if (it == client->chats.end())
            {
                return API_ENOENT;
            }
            TextChat *chat = it->second;

            // user is optional. If not provided, command apply to own user
            if (uh != INVALID_HANDLE)
            {
                if (!chat->getGroup() || (uh != client->me && chat->getOwnPrivileges() != PRIV_MODERATOR))
                {
                    return API_EACCESS;
                }
                client->removeFromChat(chatid, uh);
            }
            else
            {
                request->setParentHandle(client->me);
                client->removeFromChat(chatid, client->me);
            }
            return API_OK;
        };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::getUrlChat(MegaHandle chatid, MegaRequestListener* listener)
{
    MegaRequestPrivate* request = new MegaRequestPrivate(MegaRequest::TYPE_CHAT_URL, listener);
    request->setNodeHandle(chatid);

    request->performRequest = [this, request]()
        {
            MegaHandle chatid = request->getNodeHandle();
            if (chatid == INVALID_HANDLE)
            {
                return API_EARGS;
            }

            client->getUrlChat(chatid);
            return API_OK;
        };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::grantAccessInChat(MegaHandle chatid, MegaNode* n, MegaHandle uh, MegaRequestListener* listener)
{
    MegaRequestPrivate* request = new MegaRequestPrivate(MegaRequest::TYPE_CHAT_GRANT_ACCESS, listener);
    request->setParentHandle(chatid);
    request->setNodeHandle(n->getHandle());

    char uid[12];
    Base64::btoa((byte*)&uh, MegaClient::USERHANDLE, uid);
    uid[11] = 0;

    request->setEmail(uid);

    request->performRequest = [this, request]()
        {
            handle chatid = request->getParentHandle();
            handle h = request->getNodeHandle();
            const char *uid = request->getEmail();

            if (chatid == INVALID_HANDLE || h == INVALID_HANDLE || !uid)
            {
                return API_ENOENT;
            }

            client->grantAccessInChat(chatid, h, uid);
            return API_OK;
        };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::removeAccessInChat(MegaHandle chatid, MegaNode* n, MegaHandle uh, MegaRequestListener* listener)
{
    MegaRequestPrivate* request = new MegaRequestPrivate(MegaRequest::TYPE_CHAT_REMOVE_ACCESS, listener);
    request->setParentHandle(chatid);
    request->setNodeHandle(n->getHandle());

    char uid[12];
    Base64::btoa((byte*)&uh, MegaClient::USERHANDLE, uid);
    uid[11] = 0;

    request->setEmail(uid);

    request->performRequest = [this, request]()
        {
            handle chatid = request->getParentHandle();
            handle h = request->getNodeHandle();
            const char *uid = request->getEmail();

            if (chatid == INVALID_HANDLE || h == INVALID_HANDLE || !uid)
            {
                return API_ENOENT;
            }

            client->removeAccessInChat(chatid, h, uid);
            return API_OK;
        };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::updateChatPermissions(MegaHandle chatid, MegaHandle uh, int privilege, MegaRequestListener* listener)
{
    MegaRequestPrivate* request = new MegaRequestPrivate(MegaRequest::TYPE_CHAT_UPDATE_PERMISSIONS, listener);
    request->setNodeHandle(chatid);
    request->setParentHandle(uh);
    request->setAccess(privilege);

    request->performRequest = [this, request]()
        {
            handle chatid = request->getNodeHandle();
            handle uh = request->getParentHandle();
            int access = request->getAccess();

            if (chatid == INVALID_HANDLE || uh == INVALID_HANDLE)
            {
                return API_ENOENT;
            }
            textchat_map::iterator it = client->chats.find(chatid);
            if (it == client->chats.end())
            {
                return API_ENOENT;
            }
            TextChat *chat = it->second;
            if (!chat->getGroup() || chat->getOwnPrivileges() != PRIV_MODERATOR)
            {
                return API_EACCESS;
            }

            client->updateChatPermissions(chatid, uh, access);
            return API_OK;
        };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::truncateChat(MegaHandle chatid, MegaHandle messageid, MegaRequestListener* listener)
{
    MegaRequestPrivate* request = new MegaRequestPrivate(MegaRequest::TYPE_CHAT_TRUNCATE, listener);
    request->setNodeHandle(chatid);
    request->setParentHandle(messageid);

    request->performRequest = [this, request]()
        {
            MegaHandle chatid = request->getNodeHandle();
            handle messageid = request->getParentHandle();
            if (chatid == INVALID_HANDLE || messageid == INVALID_HANDLE)
            {
                return API_EARGS;
            }

            textchat_map::iterator it = client->chats.find(chatid);
            if (it == client->chats.end())
            {
                return API_ENOENT;
            }
            TextChat *chat = it->second;
            if (chat->getOwnPrivileges() != PRIV_MODERATOR)
            {
                return API_EACCESS;
            }

            client->truncateChat(chatid, messageid);
            return API_OK;
        };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::setChatTitle(MegaHandle chatid, const char* title, MegaRequestListener* listener)
{
    MegaRequestPrivate* request = new MegaRequestPrivate(MegaRequest::TYPE_CHAT_SET_TITLE, listener);
    request->setNodeHandle(chatid);
    request->setText(title);

    request->performRequest = [this, request]()
        {
            MegaHandle chatid = request->getNodeHandle();
            const char *title = request->getText();
            if (chatid == INVALID_HANDLE || title == NULL)
            {
                return API_EARGS;
            }

            textchat_map::iterator it = client->chats.find(chatid);
            if (it == client->chats.end())
            {
                return API_ENOENT;
            }
            TextChat *chat = it->second;
            if (!chat->getGroup() || chat->getOwnPrivileges() != PRIV_MODERATOR)
            {
                return API_EACCESS;
            }

            client->setChatTitle(chatid, title);
            return API_OK;
        };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::getChatPresenceURL(MegaRequestListener* listener)
{
    MegaRequestPrivate* request = new MegaRequestPrivate(MegaRequest::TYPE_CHAT_PRESENCE_URL, listener);

    request->performRequest = [this]()
        {
            client->getChatPresenceUrl();
            return API_OK;
        };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::registerPushNotification(int deviceType, const char* token, MegaRequestListener* listener)
{
    MegaRequestPrivate* request = new MegaRequestPrivate(MegaRequest::TYPE_REGISTER_PUSH_NOTIFICATION, listener);
    request->setNumber(deviceType);
    request->setText(token);

    request->performRequest = [this, request]()
        {
            int deviceType = int(request->getNumber());
            const char* token = request->getText();

            if ((deviceType != MegaApi::PUSH_NOTIFICATION_ANDROID &&
                deviceType != MegaApi::PUSH_NOTIFICATION_IOS_VOIP &&
                deviceType != MegaApi::PUSH_NOTIFICATION_IOS_STD &&
                deviceType != MegaApi::PUSH_NOTIFICATION_ANDROID_HUAWEI)
                || token == NULL)
            {
                return API_EARGS;
            }

            client->registerPushNotification(deviceType, token);
            return API_OK;
        };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::archiveChat(MegaHandle chatid, int archive, MegaRequestListener* listener)
{
    MegaRequestPrivate* request = new MegaRequestPrivate(MegaRequest::TYPE_CHAT_ARCHIVE, listener);
    request->setNodeHandle(chatid);
    request->setFlag(archive != 0);

    request->performRequest = [this, request]()
        {
            MegaHandle chatid = request->getNodeHandle();
            bool archive = request->getFlag();
            if (chatid == INVALID_HANDLE)
            {
                return API_ENOENT;
            }

            client->archiveChat(chatid, archive);
            return API_OK;
        };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::setChatRetentionTime(MegaHandle chatid, unsigned period, MegaRequestListener* listener)
{
    MegaRequestPrivate* request = new MegaRequestPrivate(MegaRequest::TYPE_SET_RETENTION_TIME, listener);
    request->setNodeHandle(chatid);
    request->setTotalBytes(period);

    request->performRequest = [this, request]()
        {
            MegaHandle chatid = request->getNodeHandle();
            unsigned period = static_cast <unsigned>(request->getTotalBytes());

            if (chatid == INVALID_HANDLE)
            {
                return API_EARGS;
            }

            textchat_map::iterator it = client->chats.find(chatid);
            if (it == client->chats.end())
            {
                return API_ENOENT;
            }
            TextChat *chat = it->second;
            if (chat->getOwnPrivileges() != PRIV_MODERATOR)
            {
                return API_EACCESS;
            }

            client->setchatretentiontime(chatid, period);
            return API_OK;
        };

    requestQueue.push(request);
    waiter->notify();
}

error MegaApiImpl::performRequest_chatStats(MegaRequestPrivate* request)
{
            const char* json = request->getName();
            if (!json)
            {
                return API_EARGS;
            }

            int port = int(request->getNumber());
            if (port < 0 || port > 65535)
            {
                return API_EARGS;
            }

            int type = request->getParamType();
            if (type == 1)
            {
               client->sendchatstats(json, port);
            }
            else if (type == 2)
            {
                handle userid = request->getNodeHandle();
                if (userid == UNDEF)
                {
                    return API_EARGS;
                }

                handle callid = request->getParentHandle();

                client->sendchatlogs(json, userid, callid, port);
            }
            else
            {
                return API_EARGS;
            }
            return API_OK;
}

void MegaApiImpl::requestRichPreview(const char* url, MegaRequestListener* listener)
{
    MegaRequestPrivate* request = new MegaRequestPrivate(MegaRequest::TYPE_RICH_LINK, listener);
    request->setLink(url);

    request->performRequest = [this, request]()
        {
            const char *url = request->getLink();
            if (!url)
            {
                return API_EARGS;
            }

            client->richlinkrequest(url);
            return API_OK;
        };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::chatLinkHandle(MegaHandle chatid, bool del, bool createifmissing, MegaRequestListener* listener)
{
    MegaRequestPrivate* request = new MegaRequestPrivate(MegaRequest::TYPE_CHAT_LINK_HANDLE, listener);
    request->setNodeHandle(chatid);
    request->setFlag(del);
    request->setAccess(createifmissing ? 1 : 0);

    request->performRequest = [this, request]()
        {
            MegaHandle chatid = request->getNodeHandle();
            bool del = request->getFlag();
            bool createifmissing = request->getAccess() != 0;
            if (del && createifmissing)
            {
                return API_EARGS;
            }
            if (chatid == INVALID_HANDLE)
            {
                return API_ENOENT;
            }
            textchat_map::iterator it = client->chats.find(chatid);
            if (it == client->chats.end())
            {
                return API_ENOENT;
            }
            TextChat *chat = it->second;
            if (!chat->getGroup() || !chat->publicChat() || chat->getOwnPrivileges() == PRIV_RM
                    || ((del || createifmissing) && chat->getOwnPrivileges() != PRIV_MODERATOR))
            {
                return API_EACCESS;
            }

            client->chatlink(chatid, del, createifmissing);
            return API_OK;
        };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::getChatLinkURL(MegaHandle publichandle, MegaRequestListener* listener)
{
    MegaRequestPrivate* request = new MegaRequestPrivate(MegaRequest::TYPE_CHAT_LINK_URL, listener);
    request->setNodeHandle(publichandle);

    request->performRequest = [this, request]()
        {
            MegaHandle publichandle = request->getNodeHandle();
            if (publichandle == INVALID_HANDLE)
            {
                return API_ENOENT;
            }
            client->chatlinkurl(publichandle);
            return API_OK;
        };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::chatLinkClose(MegaHandle chatid, const char* title, MegaRequestListener* listener)
{
    MegaRequestPrivate* request = new MegaRequestPrivate(MegaRequest::TYPE_SET_PRIVATE_MODE, listener);
    request->setNodeHandle(chatid);
    request->setText(title);

    request->performRequest = [this, request]()
        {
            MegaHandle chatid = request->getNodeHandle();
            const char *title = request->getText();
            if (chatid == INVALID_HANDLE)
            {
                return API_ENOENT;
            }

            textchat_map::iterator it = client->chats.find(chatid);
            if (it == client->chats.end())
            {
                return API_ENOENT;
            }
            TextChat *chat = it->second;
            if (!chat->publicChat())
            {
                return API_EEXIST;
            }
            if (!chat->getGroup() || chat->getOwnPrivileges() != PRIV_MODERATOR)
            {
                return API_EACCESS;
            }
            if (!chat->getTitle().empty() && (!title || title[0] == '\0'))
            {
                return API_EARGS;
            }

            client->chatlinkclose(chatid, title);
            return API_OK;
        };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::chatLinkJoin(MegaHandle publichandle, const char* unifiedkey, MegaRequestListener* listener)
{
    MegaRequestPrivate* request = new MegaRequestPrivate(MegaRequest::TYPE_AUTOJOIN_PUBLIC_CHAT, listener);
    request->setNodeHandle(publichandle);
    request->setSessionKey(unifiedkey);

    request->performRequest = [this, request]()
        {
            MegaHandle publichandle = request->getNodeHandle();
            const char *unifiedkey = request->getSessionKey();

            if (publichandle == INVALID_HANDLE)
            {
                return API_ENOENT;
            }

            if (unifiedkey == NULL)
            {
                return API_EINCOMPLETE;
            }
            client->chatlinkjoin(publichandle, unifiedkey);
            return API_OK;
        };

    requestQueue.push(request);
    waiter->notify();
}
#endif

void MegaApiImpl::whyAmIBlocked(bool logout, MegaRequestListener* listener)
{
    MegaRequestPrivate* request = new MegaRequestPrivate(MegaRequest::TYPE_WHY_AM_I_BLOCKED, listener);
    request->setFlag(logout);

    request->performRequest = [this]()
        {
            client->whyamiblocked();
            return API_OK;
        };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::contactLinkCreate(bool renew, MegaRequestListener* listener)
{
    MegaRequestPrivate* request = new MegaRequestPrivate(MegaRequest::TYPE_CONTACT_LINK_CREATE, listener);
    request->setFlag(renew);

    request->performRequest = [this, request]()
        {
            client->contactlinkcreate(request->getFlag());
            return API_OK;
        };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::contactLinkQuery(MegaHandle h, MegaRequestListener* listener)
{
    MegaRequestPrivate* request = new MegaRequestPrivate(MegaRequest::TYPE_CONTACT_LINK_QUERY, listener);
    request->setNodeHandle(h);

    request->performRequest = [this, request]()
        {
            handle h = request->getNodeHandle();
            if (ISUNDEF(h))
            {
                return API_EARGS;
            }

            client->contactlinkquery(h);
            return API_OK;
        };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::contactLinkDelete(MegaHandle h, MegaRequestListener* listener)
{
    MegaRequestPrivate* request = new MegaRequestPrivate(MegaRequest::TYPE_CONTACT_LINK_DELETE, listener);
    request->setNodeHandle(h);

    request->performRequest = [this, request]()
        {
            handle h = request->getNodeHandle();
            client->contactlinkdelete(h);
            return API_OK;
        };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::keepMeAlive(int type, bool enable, MegaRequestListener* listener)
{
    MegaRequestPrivate* request = new MegaRequestPrivate(MegaRequest::TYPE_KEEP_ME_ALIVE, listener);
    request->setParamType(type);
    request->setFlag(enable);

    request->performRequest = [this, request]()
        {
            int type = request->getParamType();
            bool enable = request->getFlag();

            if (type != MegaApi::KEEP_ALIVE_CAMERA_UPLOADS)
            {
                return API_EARGS;
            }

            client->keepmealive(type, enable);
            return API_OK;
        };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::getPSA(bool urlSupported, MegaRequestListener* listener)
{
    MegaRequestPrivate* request = new MegaRequestPrivate(MegaRequest::TYPE_GET_PSA, listener);
    request->setFlag(urlSupported);

    request->performRequest = [this, request]()
        {
            client->getpsa(request->getFlag());
            return API_OK;
        };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::getFolderInfo(MegaNode* node, MegaRequestListener* listener)
{
    MegaRequestPrivate* request = new MegaRequestPrivate(MegaRequest::TYPE_FOLDER_INFO, listener);
    if (node)
    {
        request->setNodeHandle(node->getHandle());
    }

    request->performRequest = [this, request]()
        {
            MegaHandle h = request->getNodeHandle();
            if (ISUNDEF(h))
            {
                return API_EARGS;
            }

            std::shared_ptr<Node> node = client->nodebyhandle(h);
            if (!node)
            {
                return API_ENOENT;
            }

            if (node->type == FILENODE)
            {
                return API_EARGS;
            }

            NodeCounter nc = node->getCounter();
            std::unique_ptr<MegaFolderInfo> folderInfo = std::make_unique<MegaFolderInfoPrivate>(
                (int)nc.files,
                node->type == FOLDERNODE ? ((int)nc.folders - 1) : (int)nc.folders,
                (int)nc.versions,
                nc.storage,
                nc.versionStorage);
            request->setMegaFolderInfo(folderInfo.get());

            fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(API_OK));
            return API_OK;
        };

    requestQueue.push(request);
    waiter->notify();
}

error MegaApiImpl::performRequest_getAchievements(MegaRequestPrivate* request)
{
            if (request->getFlag())
            {
                client->getmegaachievements(request->getAchievementsDetails());
            }
            else
            {
                client->getaccountachievements(request->getAchievementsDetails());
            }
            return API_OK;
}

void MegaApiImpl::acknowledgeUserAlerts(MegaRequestListener* listener)
{
    MegaRequestPrivate* request = new MegaRequestPrivate(MegaRequest::TYPE_USERALERT_ACKNOWLEDGE, listener);

    request->performRequest = [this]()
        {
            client->acknowledgeuseralerts();
            return API_OK;
        };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::getPublicLinkInformation(const char* megaFolderLink, MegaRequestListener* listener)
{
    MegaRequestPrivate* request = new MegaRequestPrivate(MegaRequest::TYPE_PUBLIC_LINK_INFORMATION, listener);
    request->setLink(megaFolderLink);

    request->performRequest = [this, request]()
        {
            const char *link = request->getLink();
            if (!link)
            {
                return API_EARGS;
            }

            handle h = UNDEF;
            byte folderkey[FOLDERNODEKEYLENGTH];
            error e = client->parsepubliclink(link, h, folderkey, TypeOfLink::FOLDER);
            if (e == API_OK)
            {
                request->setNodeHandle(h);
                Base64Str<FOLDERNODEKEYLENGTH> folderkeyB64(folderkey);
                request->setPrivateKey(folderkeyB64.chars);
                client->getpubliclinkinfo(h);
            }
            return e;
        };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::getFileAttributeUploadURL(MegaHandle nodehandle,
                                            int64_t fullFileSize,
                                            int faType,
                                            bool forceSSL,
                                            MegaRequestListener* listener)
{
    MegaRequestPrivate* request = new MegaRequestPrivate(MegaRequest::TYPE_GET_FA_UPLOAD_URL, listener);
    request->setNodeHandle(nodehandle);
    request->setNumber(fullFileSize);
    request->setFlag(forceSSL);
    request->setParamType(faType);

    request->performRequest = [this, request]()
        {
            bool getIp = true;
            uint64_t nodeHandle = request->getNodeHandle();
            int intFaType = request->getParamType();
            assert(intFaType >= 0 && (intFaType < (1 << (sizeof(fatype)*8)))); // Value of intFaType <= (2^(fatype_numbits) - 1)
            fatype faType =
                static_cast<fatype>(intFaType); // if the assert above is true, int should fit fine
                                                // into a fatype (uint16_t)
            bool forceSSL = request->getFlag();
            size_t fullSize = static_cast<size_t>(request->getNumber());

            NodeOrUploadHandle nuh(NodeHandle().set6byte(nodeHandle));

            client->queueCommand(new CommandPutFA(
                std::move(nuh),
                faType,
                forceSSL,
                -1,
                fullSize,
                getIp,
                [this,
                 request](Error e, const std::string& url, const std::vector<std::string>& ips)
                {
                    assert(e != API_OK || !url.empty());
                    if (e == API_OK && !url.empty())
                    {
                        request->setName(url.c_str());
                        if (!ips.empty())
                        {
                            request->setLink(ips.at(0).c_str());
                        }
                        if (ips.size() > 1)
                        {
                            request->setText(ips.at(1).c_str());
                        }
                    }
                    fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e));
                }));
            return API_OK;
        };

    requestQueue.push(request);
    waiter->notify();
}

error MegaApiImpl::performRequest_getBackgroundUploadURL(MegaRequestPrivate* request)
{
            //if not using MegaBackgroundMediaUpload, ask for IPs
            bool getIp = !request->getMegaBackgroundMediaUploadPtr();

            client->queueCommand(new CommandGetPutUrl(
                request->getNumber(),
                request->getFlag(),
                getIp,
                [this,
                 request](Error e, const std::string& url, const std::vector<std::string>& ips)
                {
                    assert(e != API_OK || !url.empty());
                    if (e == API_OK && !url.empty())
                    {
                        if (request->getMegaBackgroundMediaUploadPtr())
                        {
                            MegaBackgroundMediaUploadPrivate* mu =
                                static_cast<MegaBackgroundMediaUploadPrivate*>(
                                    request->getMegaBackgroundMediaUploadPtr());
                            mu->url = url;
                        }
                        else
                        {
                            request->setName(url.c_str());
                            if (!ips.empty())
                            {
                                request->setLink(ips.at(0).c_str());
                            }
                            if (ips.size() > 1)
                            {
                                request->setText(ips.at(1).c_str());
                            }
                        }
                    }
                    fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e));
                }));
            return API_OK;
}

error MegaApiImpl::performRequest_completeBackgroundUpload(MegaRequestPrivate* request)
{
            MegaBackgroundMediaUploadPrivate* bgMediaUpload = static_cast<MegaBackgroundMediaUploadPrivate*>(request->getMegaBackgroundMediaUploadPtr());
            const char * base64fileKey = request->getPrivateKey();

            if (!base64fileKey && !bgMediaUpload)
            {
                return API_EINCOMPLETE;
            }

            const char* utf8Name = request->getName();
            auto parentHandle = NodeHandle().set6byte(request->getParentHandle());
            const char *uploadToken = request->getSessionKey();
            const char* fingerprintOriginal = request->getPassword();
            const char* fingerprint = request->getNewPassword();

            if (!fingerprint || !utf8Name || !uploadToken || parentHandle.isUndef())
            {
                return API_EINCOMPLETE;
            }

            UploadToken ulToken;
            auto binTokSize = Base64::atob(uploadToken, &ulToken[0], sizeof(ulToken));
            if (binTokSize != 36 || binTokSize != sizeof(ulToken))
            {
                LOG_err << "Invalid upload token: " << uploadToken;
                return API_EARGS;
            }

            byte *theFileKey;
            std::unique_ptr<byte[]> filekey;
            if (bgMediaUpload) //using MegaBackgroundMediaUploadPrivate key
            {
                theFileKey = bgMediaUpload->filekey;
            }
            else //app driven upload (key provided as parameter)
            {
                theFileKey = new byte[FILENODEKEYLENGTH];
                auto binFileKeySize = Base64::atob(base64fileKey, theFileKey, FILENODEKEYLENGTH);
                filekey.reset(theFileKey);

                if (binFileKeySize != FILENODEKEYLENGTH)
                {
                    LOG_err << "Invalid file key";
                    return API_EARGS;
                }
            }

            std::shared_ptr<Node> parentNode = client->nodeByHandle(parentHandle);
            if (!parentNode)
            {
                LOG_err << "Parent node doesn't exist anymore";
                return API_ENOENT;
            }

            const string megafingerprint = MegaNodePrivate::removeAppPrefixFromFingerprint(fingerprint);
            if (megafingerprint.empty())
            {
                LOG_err << "Bad fingerprint";
                return API_EARGS;
            }

            std::function<error(string&)> addFileAttrsFunc;
            std::function<error(AttrMap& attrs)> addNodeAttrsFunc;

            if (bgMediaUpload)
            {
                addFileAttrsFunc = [bgMediaUpload](string& fileattributes)
                {
                    appendFileAttribute(fileattributes,
                                        GfxProc::THUMBNAIL,
                                        bgMediaUpload->thumbnailFA);
                    appendFileAttribute(fileattributes, GfxProc::PREVIEW, bgMediaUpload->previewFA);

#ifdef USE_MEDIAINFO
                    if (bgMediaUpload->mediaproperties.isPopulated())
                    {
                        if (!fileattributes.empty())
                        {
                            fileattributes.append("/");
                        }
                        fileattributes.append(MediaProperties::encodeMediaPropertiesAttributes(
                            bgMediaUpload->mediaproperties,
                            (uint32_t*)(bgMediaUpload->filekey + FILENODEKEYLENGTH / 2)));
                    }
#endif
                    return API_OK;
                };

                addNodeAttrsFunc = [this, bgMediaUpload](AttrMap& attrs)
                {
                    error e = API_OK;
                    int lat, lon;
                    encodeCoordinates(bgMediaUpload->latitude, bgMediaUpload->longitude, lat, lon);
                    attr_map updates;
                    if (API_OK != (e = updateAttributesMapWithCoordinates(updates, lat, lon, bgMediaUpload->unshareableGPS, client)))
                    {
                        return e;
                    }
                    attrs.applyUpdates(updates);
                    return e;
                };
            }

            vector<NewNode> newnodes(1);
            NewNode* newnode = &newnodes[0];
            error e = client->putnodes_prepareOneFile(newnode, parentNode.get(), utf8Name, ulToken,
                                                theFileKey, megafingerprint.c_str(), fingerprintOriginal,
                                                std::move(addNodeAttrsFunc),
                                                std::move(addFileAttrsFunc));
            if (e != API_OK)
            {
                return e;
            }

            Pitag pitag{PitagPurpose::Upload,
                        PitagTrigger::Camera,
                        PitagNodeType::File,
                        PitagTarget::CloudDrive,
                        PitagImportSource::NotApplicable};

            const bool inIncomingShare = parentNode && parentNode->matchesOrHasAncestorMatching(
                                                           [](const Node& n)
                                                           {
                                                               return n.inshare != nullptr;
                                                           });

            pitag.target = inIncomingShare ? PitagTarget::IncomingShare : pitag.target;

            client->queueCommand(new CommandPutNodes(client,
                                                     parentHandle,
                                                     NULL,
                                                     UseLocalVersioningFlag,
                                                     std::move(newnodes),
                                                     request->getTag(),
                                                     PUTNODES_APP,
                                                     nullptr,
                                                     nullptr,
                                                     false,
                                                     {}, // customerIpPort
                                                     pitag));
            return e;
}

error MegaApiImpl::performRequest_verifyCredentials(MegaRequestPrivate* request)
{
            handle uh = request->getNodeHandle();
            bool isReset = request->getFlag();
            std::function<void(Error)> completion = [this, request](Error e)
            {
                fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e));
            };

            if (isReset)
            {
                return client->resetCredentials(uh, std::move(completion));
            }
            else
            {
                return client->verifyCredentials(uh, std::move(completion));
            }
}

void MegaApiImpl::sendSMSVerificationCode(const char* phoneNumber, MegaRequestListener* listener, bool reverifying_whitelisted)
{
    MegaRequestPrivate* request = new MegaRequestPrivate(MegaRequest::TYPE_SEND_SMS_VERIFICATIONCODE, listener);
    request->setText(phoneNumber);
    request->setFlag(reverifying_whitelisted);

    request->performRequest = [this, request]()
        {
            string phoneNumber = request->getText();
            bool reverifying_whitelisted = request->getFlag();

            return client->smsverificationsend(phoneNumber, reverifying_whitelisted);
        };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::checkSMSVerificationCode(const char* verificationCode, MegaRequestListener* listener)
{
    MegaRequestPrivate* request = new MegaRequestPrivate(MegaRequest::TYPE_CHECK_SMS_VERIFICATIONCODE, listener);
    request->setText(verificationCode);

    request->performRequest = [this, request]()
        {
            string code = request->getText();
            error e = client->smsverificationcheck(code);
            // FIXME: if the API returned the new state and the verified phone number in
            // the response to the code's verification, the following block can be deleted
            if (e == API_OK)
            {
                client->queueCommand(new CommandGetUserData(client, client->reqtag, nullptr));
            }
            return e;
        };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::getCountryCallingCodes(MegaRequestListener* listener)
{
    MegaRequestPrivate* request = new MegaRequestPrivate(MegaRequest::TYPE_GET_COUNTRY_CALLING_CODES, listener);

    request->performRequest = [this]()
    {
        client->queueCommand(new CommandGetCountryCallingCodes{client});
        return API_OK;
    };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::getMiscFlags(MegaRequestListener* listener)
{
    MegaRequestPrivate* request = new MegaRequestPrivate(MegaRequest::TYPE_GET_MISC_FLAGS, listener);

    request->performRequest = [this]()
        {
            if (client->loggedin())
            {
                // it only returns not-user-related flags (ie. server-sider-rubbish scheduler is missing)
                return API_EACCESS;
            }
            client->getmiscflags();
            return API_OK;
        };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::getBanners(MegaRequestListener* listener)
{
    MegaRequestPrivate* request = new MegaRequestPrivate(MegaRequest::TYPE_GET_BANNERS, listener);

    request->performRequest = [this]()
    {
        client->queueCommand(new CommandGetBanners(client));
        return API_OK;
    };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::dismissBanner(int id, MegaRequestListener* listener)
{
    MegaRequestPrivate* request = new MegaRequestPrivate(MegaRequest::TYPE_DISMISS_BANNER, listener);
    request->setParamType(id); // banner id
    request->setNumber(m_time(nullptr)); // timestamp

    request->performRequest = [this, request]()
    {
        client->queueCommand(
            new CommandDismissBanner(client, request->getParamType(), request->getNumber()));
        return API_OK;
    };

    requestQueue.push(request);
    waiter->notify();
}

error MegaApiImpl::performRequest_backupPut(MegaRequestPrivate* request)
{
    if (!client->loggedin()) return API_EACCESS;    // it requires master key to encrypt data, which is received in response to login

            NodeHandle remoteNode = NodeHandle().set6byte(request->getNodeHandle());
            const char* backupName = request->getName();
            const char* localFolder = request->getFile();
            int backupType = static_cast<int>(request->getTotalBytes());
            BackupType bType = static_cast<BackupType>(backupType);
            handle backupId = request->getParentHandle();

            CommandBackupPut::BackupInfo info;
            info.backupId = backupId;
            info.type = bType;
            info.backupName = backupName ? backupName : "";
            info.nodeHandle = remoteNode;
            info.localFolder = localFolder ? LocalPath::fromAbsolutePath(localFolder) : LocalPath();
            info.deviceId = client->getDeviceidHash();
            if (info.deviceId.empty())
            {
                LOG_err << "Failed to get Device ID while handling backup " << info.backupName;
                return API_EARGS;
            }
            info.state = CommandBackupPut::SPState(request->getAccess());
            info.subState = request->getNumDetails();

            bool isNew = request->getFlag();
            if (isNew) // Register a new sync/backup
            {
                if ((backupType != MegaApi::BACKUP_TYPE_CAMERA_UPLOADS
                     && backupType != MegaApi::BACKUP_TYPE_MEDIA_UPLOADS)
                        || !localFolder
                        || !backupName
                        || remoteNode.isUndef()
                        || !ISUNDEF(backupId))  // new backups don't have a backup id yet
                {
                    return API_EARGS;
                }

                client->queueCommand(new CommandBackupPut(client, info, nullptr));
            }
            else // update an existing sync/backup
            {
                if ((backupType != MegaApi::BACKUP_TYPE_CAMERA_UPLOADS
                    && backupType != MegaApi::BACKUP_TYPE_MEDIA_UPLOADS
                    && backupType != MegaApi::BACKUP_TYPE_INVALID)
                    || ISUNDEF(backupId))   // existing backups must have a backup id
                {
                    return API_EARGS;
                }

                client->queueCommand(new CommandBackupPut(client, info, nullptr));
            }
            return API_OK;
}

void MegaApiImpl::removeBackup(MegaHandle backupId, MegaRequestListener* listener)
{
    MegaRequestPrivate* request = new MegaRequestPrivate(MegaRequest::TYPE_BACKUP_REMOVE, listener);
    request->setParentHandle(backupId);

    request->performRequest = [this, request]()
    {
        client->queueCommand(new CommandBackupRemove(
            client,
            request->getParentHandle(),
            [request, this](Error e)
            {
                fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e));
            }));
        return API_OK;
    };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::removeFromBC(MegaHandle backupId, MegaHandle moveDestination, MegaRequestListener* listener)
{
    MegaRequestPrivate* request = new MegaRequestPrivate(MegaRequest::TYPE_BACKUP_REMOVE_MD, listener);
    request->setParentHandle(backupId);
    request->setNodeHandle(moveDestination);

    request->performRequest = [this, request]()
    {
        auto finalCompletion = [this, request](Error e) { fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e)); };

        client->removeFromBC(request->getParentHandle(), request->getNodeHandle(), finalCompletion);
        return API_OK;
    };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::pauseFromBC(MegaHandle backupId, MegaRequestListener* listener)
{
    MegaRequestPrivate* request = new MegaRequestPrivate(MegaRequest::TYPE_BACKUP_PAUSE_MD, listener);
    request->setParentHandle(backupId);

    request->performRequest = [this, request]()
    {
        auto finalCompletion = [this, request](const Error& e) { fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e)); };

        client->updateStateInBC(request->getParentHandle(), CommandBackupPut::TEMPORARY_DISABLED, finalCompletion);
        return API_OK;
    };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::resumeFromBC(MegaHandle backupId, MegaRequestListener* listener)
{
    MegaRequestPrivate* request = new MegaRequestPrivate(MegaRequest::TYPE_BACKUP_RESUME_MD, listener);
    request->setParentHandle(backupId);

    request->performRequest = [this, request]()
    {
        auto finalCompletion = [this, request](const Error& e) { fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e)); };

        client->updateStateInBC(request->getParentHandle(), CommandBackupPut::ACTIVE, finalCompletion);
        return API_OK;
    };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::getBackupInfo(MegaRequestListener* listener)
{
    MegaRequestPrivate* request = new MegaRequestPrivate(MegaRequest::TYPE_BACKUP_INFO, listener);

    request->performRequest = [this, request]()
        {
            client->getBackupInfo([this, request](const Error& e, const vector<CommandBackupSyncFetch::Data>& d)
            {
                if (e == API_OK)
                {
                    request->setMegaBackupInfoList(std::make_unique<MegaBackupInfoListPrivate>(d));
                }

                fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e));
            });

            return API_OK;
        };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::sendBackupHeartbeat(MegaHandle backupId, int status, int progress, int ups, int downs, long long ts, MegaHandle lastNode, MegaRequestListener* listener)
{
    MegaRequestPrivate* request = new MegaRequestPrivate(MegaRequest::TYPE_BACKUP_PUT_HEART_BEAT, listener);
    request->setParentHandle(backupId);
    request->setAccess(status);
    request->setNumDetails(progress);
    request->setParamType(ups);
    request->setTransferTag(downs);
    request->setNumber(ts);
    request->setNodeHandle(lastNode);

    request->performRequest = [this, request]()
    {
        client->queueCommand(new CommandBackupPutHeartBeat(
            client,
            (MegaHandle)request->getParentHandle(),
            CommandBackupPutHeartBeat::SPHBStatus(request->getAccess()),
            static_cast<int8_t>(request->getNumDetails()),
            (uint32_t)request->getParamType(),
            (uint32_t)request->getTransferTag(),
            request->getNumber(),
            request->getNodeHandle(),
            [this, request](Error e)
            {
                fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e));
            }));
        return API_OK;
    };

    requestQueue.push(request);
    waiter->notify();
}

#ifdef ENABLE_CHAT
void MegaApiImpl::startChatCall(MegaHandle chatid, bool notRinging, MegaRequestListener* listener)
{
    MegaRequestPrivate* request = new MegaRequestPrivate(MegaRequest::TYPE_START_CHAT_CALL, listener);
    request->setNodeHandle(chatid);
    request->setFlag(notRinging);

    request->performRequest = [this, request]()
        {
            handle chatid = request->getNodeHandle();
            if (chatid == INVALID_HANDLE)
            {
                return API_EARGS;
            }

            const bool notRinging = request->getFlag();
            client->queueCommand(new CommandMeetingStart(
                client,
                chatid,
                notRinging,
                [request, this](Error e, std::string sfuUrl, handle callid)
                {
                    if (e == API_OK)
                    {
                        request->setParentHandle(callid);
                        request->setText(sfuUrl.c_str());
                    }

                    fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e));
                }));
            return API_OK;
        };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::joinChatCall(MegaHandle chatid, MegaHandle callid, MegaRequestListener* listener)
{
    MegaRequestPrivate* request = new MegaRequestPrivate(MegaRequest::TYPE_JOIN_CHAT_CALL, listener);
    request->setNodeHandle(chatid);
    request->setParentHandle(callid);

    request->performRequest = [this, request]()
        {
            handle chatid = request->getNodeHandle();
            handle callid = request->getParentHandle();
            if (chatid == INVALID_HANDLE || callid == INVALID_HANDLE)
            {
                return API_EARGS;
            }

            client->queueCommand(new CommandMeetingJoin(
                client,
                chatid,
                callid,
                [request, this](Error e, std::string sfuUrl)
                {
                    if (e == API_OK)
                    {
                        request->setText(sfuUrl.c_str());
                    }

                    fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e));
                }));
            return API_OK;
        };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::endChatCall(MegaHandle chatid, MegaHandle callid, int reason, MegaRequestListener* listener)
{
    MegaRequestPrivate* request = new MegaRequestPrivate(MegaRequest::TYPE_END_CHAT_CALL, listener);
    request->setNodeHandle(chatid);
    request->setParentHandle(callid);
    request->setAccess(reason);

    request->performRequest = [this, request]()
        {
            handle chatid = request->getNodeHandle();
            handle callid = request->getParentHandle();
            int reason = request->getAccess();
            if (chatid == INVALID_HANDLE
                    || callid == INVALID_HANDLE
                    || !client->isValidEndCallReason(reason))
            {
                return API_EARGS;
            }

            textchat_map::iterator it = client->chats.find(chatid);
            if (it == client->chats.end())
            {
                return API_ENOENT;
            }

            TextChat* chat = it->second;
            if (reason == END_CALL_REASON_BY_MODERATOR && !chat->getGroup())
            {
                return API_EACCESS;
            }

            client->queueCommand(new CommandMeetingEnd(
                client,
                chatid,
                callid,
                reason,
                [request, this](Error e)
                {
                    fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e));
                }));
            return API_OK;
        };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::ringIndividualInACall(MegaHandle chatid, MegaHandle userid, MegaRequestListener* listener)
{
    MegaRequestPrivate* request = new MegaRequestPrivate(MegaRequest::TYPE_RING_INDIVIDUAL_IN_CALL, listener);
    request->setNodeHandle(chatid);
    request->setParentHandle(userid);
    request->performRequest = [this, request]()
    {
        const handle chatid = request->getNodeHandle();
        const handle userid = request->getParentHandle();
        if (chatid == INVALID_HANDLE || userid == INVALID_HANDLE)
        {
            return API_EARGS;
        }

        textchat_map::iterator it = client->chats.find(chatid);
        if (it == client->chats.end())
        {
            return API_ENOENT;
        }

        client->queueCommand(new CommandRingUser(
            client,
            chatid,
            userid,
            [request, this](Error e)
            {
                fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e));
            }));
        return API_OK;
    };

    requestQueue.push(request);
    waiter->notify();
}
#endif

void MegaApiImpl::setMyBackupsFolder(const char* localizedName, MegaRequestListener* listener)
{
    MegaRequestPrivate* request = new MegaRequestPrivate(MegaRequest::TYPE_SET_MY_BACKUPS, listener);
    request->setText(localizedName);

    request->performRequest = [this, request]()
        {
            auto addua_completion = [request, this](Error e)
            {
                if (e == API_OK)
                {
                    const UserAttribute* attribute =
                        client->ownuser()->getAttribute(ATTR_MY_BACKUPS_FOLDER);
                    if (attribute && !attribute->isNotExisting())
                    {
                        assert(attribute->value().size() == MegaClient::NODEHANDLE);
                        handle h = 0;
                        memcpy(&h, attribute->value().data(), MegaClient::NODEHANDLE);
                        request->setNodeHandle(h);
                    }
                    else
                    {
                        LOG_err << "Invalid value for ATTR_MY_BACKUPS_FOLDER";
                        e = API_EINTERNAL;
                    }
                }
                fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e));
            };

            return client->setbackupfolder(request->getText(), request->getTag(), addua_completion);
        };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::getRecentActionsAsync(unsigned days,
                                        unsigned maxnodes,
                                        MegaRequestListener* listener)
{
    getRecentActionsAsyncInternal(days, maxnodes, nullptr, listener);
}

void MegaApiImpl::getRecentActionsAsync(unsigned days,
                                        unsigned maxnodes,
                                        bool excludeSensitives,
                                        MegaRequestListener* listener)
{
    getRecentActionsAsyncInternal(days, maxnodes, &excludeSensitives, listener);
}

void MegaApiImpl::getRecentActionByIdInternal(const char* id,
                                              std::optional<bool> excludeSensitives,
                                              MegaRequestListener* listener)
{
    MegaRequestPrivate* request =
        new MegaRequestPrivate(MegaRequest::TYPE_GET_RECENT_ACTION_BY_ID, listener);
    if (id)
    {
        request->setText(id);
    }
    if (excludeSensitives.has_value())
    {
        request->setFlag(*excludeSensitives);
    }

    request->performRequest =
        [this, request, hasExcludeSensitives = excludeSensitives.has_value()]()
    {
        const char* id = request->getText();
        recentaction ra;
        error e;
        if (hasExcludeSensitives)
        {
            const bool excl = request->getFlag();
            e = client->getRecentActionById(id, excl, ra);
        }
        else
        {
            e = client->getRecentActionById(id, ra);
        }
        if (e != API_OK)
        {
            return e;
        }
        recentactions_vector rav;
        rav.emplace_back(std::move(ra));

        std::unique_ptr<MegaRecentActionBucketList> recentActions(
            new MegaRecentActionBucketListPrivate(rav));
        request->setRecentActions(std::move(recentActions));
        fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(API_OK));
        return API_OK;
    };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::getRecentActionById(const char* id, MegaRequestListener* listener)
{
    getRecentActionByIdInternal(id, std::nullopt, listener);
}

void MegaApiImpl::getRecentActionById(const char* id,
                                      bool excludeSensitives,
                                      MegaRequestListener* listener)
{
    getRecentActionByIdInternal(id, std::make_optional(excludeSensitives), listener);
}

void MegaApiImpl::getRecentActionsAsyncInternal(unsigned days,
                                                unsigned maxnodes,
                                                bool* optExcludeSensitives,
                                                MegaRequestListener* listener)
{
    MegaRequestPrivate* request = new MegaRequestPrivate(MegaRequest::TYPE_GET_RECENT_ACTIONS, listener);
    request->setNumber(days);
    request->setParamType(static_cast<int>(maxnodes));
    if (optExcludeSensitives)
        request->setFlag(*optExcludeSensitives);

    request->performRequest =
        [this, request, withExcludeSensitives = (optExcludeSensitives != nullptr)]()
    {
        unsigned maxnodes = static_cast<unsigned>(request->getParamType());
        if (maxnodes == 0)
        {
            return API_EARGS;
        }

        int days = static_cast<int>(request->getNumber());
        if (days <= 0)
        {
            return API_EARGS;
        }

        m_time_t since = m_time() - days * 86400;

        MegaTimeStamp sinceClearHistory = getRecentClearTimestamp();

        since = std::max(since, sinceClearHistory);

        recentactions_vector v;
        if (withExcludeSensitives)
        {
            bool excludeSensitives = request->getFlag();
            v = client->getRecentActions(maxnodes, since, excludeSensitives);
        }
        else
        {
            v = client->getRecentActions(maxnodes, since);
        }
        std::unique_ptr<MegaRecentActionBucketList> recentActions(
            new MegaRecentActionBucketListPrivate(v));
        request->setRecentActions(std::move(recentActions));
        fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(API_OK));
        return API_OK;
    };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::clearRecentActionHistory(MegaTimeStamp until, MegaRequestListener* listener)
{
    MegaRequestPrivate* request = new MegaRequestPrivate(MegaRequest::TYPE_SET_ATTR_USER, listener);
    request->setParamType(MegaApi::USER_ATTR_RECENT_CLEAR_TIMESTAMP);
    request->setNumber(until);

    request->performRequest = [this, request]()
    {
        MegaTimeStamp time = request->getNumber();
        if (!MegaClient::isValidMegaTimeStamp(time))
        {
            return API_EARGS;
        }
        MegaStringMapPrivate stringMap;
        string key = "t";
        auto b64 = Base64Str<sizeof(time)>(reinterpret_cast<void*>(&time));
        stringMap.set(key.c_str(), (Base64::btoa(b64.chars)).c_str());
        request->setMegaStringMap(&stringMap);

        return performRequest_setAttrUser(request);
    };

    requestQueue.push(request);
    waiter->notify();
}

MegaTimeStamp MegaApiImpl::getRecentClearTimestamp()
{
    mega::User* user = client->finduser(client->me);
    if (user == nullptr)
    {
        return MEGA_INVALID_TIMESTAMP;
    }

    const UserAttribute* attr = user->getAttribute(ATTR_RECENT_CLEAR_TIMESTAMP);
    if (attr == nullptr || attr->value().empty())
    {
        return MEGA_INVALID_TIMESTAMP;
    }

    std::unique_ptr<string_map> records{tlv::containerToRecords(attr->value(), client->key)};
    if (!records || records->empty())
    {
        return MEGA_INVALID_TIMESTAMP;
    }
    return formatRecentClearTimestamp(records.get());
}

MegaTimeStamp MegaApiImpl::formatRecentClearTimestamp(string_map* records)
{
    MegaTimeStamp recentClearTimestamp = MEGA_INVALID_TIMESTAMP;
    auto it = records->find("t");
    if (it == records->end())
    {
        return recentClearTimestamp;
    }
    string value = Base64::atob(it->second);
    if (value.size() != sizeof(recentClearTimestamp))
    {
        return recentClearTimestamp;
    }
    memcpy(&recentClearTimestamp, value.data(), sizeof(recentClearTimestamp));
    return recentClearTimestamp;
}

#ifdef ENABLE_CHAT
void MegaApiImpl::createOrUpdateScheduledMeeting(const MegaScheduledMeeting* scheduledMeeting, const char* chatTitle, MegaRequestListener* listener)
{
    MegaRequestPrivate* request = new MegaRequestPrivate(MegaRequest::TYPE_ADD_UPDATE_SCHEDULED_MEETING, listener);
    assert(scheduledMeeting);
    if (scheduledMeeting)
    {
        std::unique_ptr<MegaScheduledMeetingList> l(MegaScheduledMeetingList::createInstance());
        l->insert(scheduledMeeting->copy());
        request->setMegaScheduledMeetingList(l.get());
    }
    request->setText(chatTitle);

    request->performRequest = [this, request]()
        {
            if (!request->getMegaScheduledMeetingList() || request->getMegaScheduledMeetingList()->size() != 1)
            {
                return API_EARGS;
            }

            MegaScheduledMeetingPrivate* schedMeeting = static_cast<MegaScheduledMeetingPrivate*>(request->getMegaScheduledMeetingList()->at(0));
            handle chatid = schedMeeting->chatid();
            textchat_map::iterator it = client->chats.find(chatid);
            if (it == client->chats.end())
            {
                return API_ENOENT;
            }

            const char* chatTitle = request->getText();
            if (chatTitle && !strlen(chatTitle))
            {
                return API_EARGS;
            }

            client->queueCommand(new CommandScheduledMeetingAddOrUpdate(
                client,
                schedMeeting->scheduledMeeting(),
                chatTitle,
                [request, this](Error e, const ScheduledMeeting* sm)
                {
                    if (sm)
                    {
                        std::unique_ptr<MegaScheduledMeetingList> l(
                            MegaScheduledMeetingList::createInstance());
                        l->insert(new MegaScheduledMeetingPrivate(sm));
                        request->setMegaScheduledMeetingList(l.get());
                    }

                    fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e));
                }));
            return API_OK;
        };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::removeScheduledMeeting(MegaHandle chatid, MegaHandle schedId, MegaRequestListener* listener)
{
    MegaRequestPrivate* request = new MegaRequestPrivate(MegaRequest::TYPE_DEL_SCHEDULED_MEETING, listener);
    request->setNodeHandle(chatid);
    request->setParentHandle(schedId);

    request->performRequest = [this, request]()
        {
            handle chatid = request->getNodeHandle();
            handle schedId = request->getParentHandle();

            textchat_map::iterator it = client->chats.find(chatid);
            if (it == client->chats.end())
            {
                return API_ENOENT;
            }

            client->queueCommand(new CommandScheduledMeetingRemove(
                client,
                chatid,
                schedId,
                [request, this](Error e)
                {
                    fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e));
                }));
            return API_OK;
        };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::fetchScheduledMeeting(MegaHandle chatid, MegaHandle schedId, MegaRequestListener* listener)
{
    MegaRequestPrivate* request = new MegaRequestPrivate(MegaRequest::TYPE_FETCH_SCHEDULED_MEETING, listener);
    request->setNodeHandle(chatid);
    request->setParentHandle(schedId);

    request->performRequest = [this, request]()
        {
            handle chatid = request->getNodeHandle();
            handle schedId = request->getParentHandle();

            textchat_map::iterator it = client->chats.find(chatid);
            if (it == client->chats.end())
            {
                return API_ENOENT;
            }

            client->queueCommand(new CommandScheduledMeetingFetch(
                client,
                chatid,
                schedId,
                [request, this](Error e,
                                const std::vector<std::unique_ptr<ScheduledMeeting>>* result)
                {
                    if (result && !result->empty())
                    {
                        std::unique_ptr<MegaScheduledMeetingList> l(
                            MegaScheduledMeetingList::createInstance());
                        for (auto const& sm: *result)
                        {
                            l->insert(new MegaScheduledMeetingPrivate(sm.get()));
                        }
                        request->setMegaScheduledMeetingList(l.get());
                    }

                    fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e));
                }));
            return API_OK;
        };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::fetchScheduledMeetingEvents(MegaHandle chatid, MegaTimeStamp since, MegaTimeStamp until, unsigned int count, MegaRequestListener* listener)
{
    MegaRequestPrivate* request = new MegaRequestPrivate(MegaRequest::TYPE_FETCH_SCHEDULED_MEETING_OCCURRENCES, listener);
    request->setNodeHandle(chatid);
    request->setNumber(since);
    request->setTotalBytes(until);
    request->setTransferredBytes(count);

    request->performRequest = [this, request]()
        {
            handle chatid = request->getNodeHandle();
            m_time_t since = request->getNumber();
            m_time_t until = request->getTotalBytes();
            unsigned int count = static_cast<unsigned int>(request->getTransferredBytes());

            textchat_map::iterator it = client->chats.find(chatid);
            if (it == client->chats.end())
            {
                return API_ENOENT;
            }

            client->queueCommand(new CommandScheduledMeetingFetchEvents(
                client,
                chatid,
                since,
                until,
                count,
                true /*byDemand*/,
                [request, this](Error e,
                                const std::vector<std::unique_ptr<ScheduledMeeting>>* result)
                {
                    if (result && !result->empty())
                    {
                        std::unique_ptr<MegaScheduledMeetingList> l(
                            MegaScheduledMeetingList::createInstance());
                        for (auto const& sm: *result)
                        {
                            l->insert(new MegaScheduledMeetingPrivate(sm.get()));
                        }
                        request->setMegaScheduledMeetingList(l.get());
                    }
                    fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e));
                }));
            return API_OK;
        };

    requestQueue.push(request);
    waiter->notify();
}
#endif

unsigned long long MegaApiImpl::getNumNodes()
{
    return client->totalNodes.load();
}

unsigned long long MegaApiImpl::getAccurateNumNodes()
{
    SdkMutexGuard g(sdkMutex);
    return client->totalNodes.load();
}

void MegaApiImpl::setLRUCacheSize(unsigned long long size)
{
    client->mNodeManager.setCacheLRUMaxSize(size);
}

unsigned long long MegaApiImpl::getNumNodesAtCacheLRU() const
{
    return client->mNodeManager.getNumNodesAtCacheLRU();
}

bool MegaApiImpl::isSyncStalled()
{
    // no need to lock sdkMutex for these simple flags
#ifdef ENABLE_SYNC
    if (receivedStallFlag.load()) return true;
    if (receivedNameConflictsFlag.load()) return true;
#endif
    return false;
}

bool MegaApiImpl::isSyncStalledChanged()
{
    // no need to lock sdkMutex for these simple flags
#ifdef ENABLE_SYNC
    if (receivedTotalStallsFlag.load()) return true;
    if (receivedTotalNameConflictsFlag.load()) return true;
#endif
    return false;
}

int MegaApiImpl::isWaiting()
{
    // no need to lock sdkMutex for these simple flags
    if (waitingRequest)
    {
        LOG_debug << "SDK waiting for a request. Reason: " << waitingRequest;
    }
    return waitingRequest;
}

void MegaApiImpl::lockMutex()
{
    sdkMutex.lock();
}

void MegaApiImpl::unlockMutex()
{
    sdkMutex.unlock();
}

bool MegaApiImpl::tryLockMutexFor(long long time)
{
    if (time <= 0)
    {
        return sdkMutex.try_lock();
    }
    else
    {
        return sdkMutex.try_lock_for(std::chrono::milliseconds(time));
    }
}

void MegaApiImpl::setBackup(int backupType, MegaHandle targetNode, const char* localFolder, const char* backupName, int state, int subState, MegaRequestListener* listener)
{
    MegaRequestPrivate *request = new MegaRequestPrivate(MegaRequest::TYPE_BACKUP_PUT, listener);
    request->setTotalBytes(backupType);
    request->setNodeHandle(targetNode);
    request->setFile(localFolder);
    request->setName(backupName);
    request->setAccess(state);
    request->setNumDetails(subState);
    request->setFlag(true); // indicates it's a new backup

    request->performRequest = [this, request]()
    {
        return performRequest_backupPut(request);
    };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::updateBackup(MegaHandle backupId, int backupType, MegaHandle targetNode, const char* localFolder,  const char* backupName, int state, int subState, MegaRequestListener* listener)
{
    MegaRequestPrivate* request = new MegaRequestPrivate(MegaRequest::TYPE_BACKUP_PUT, listener);
    request->setParentHandle(backupId);

    if (backupType != BackupType::INVALID)
    {
        request->setTotalBytes(backupType);
    }

    if (targetNode != UNDEF)
    {
        request->setNodeHandle(targetNode);
    }

    if (localFolder)
    {
        request->setFile(localFolder);
    }

    if (backupName)
    {
        request->setName(backupName);
    }

    if (state >= 0)
    {
        request->setAccess(state);
    }

    if (subState >= 0)
    {
        request->setNumDetails(subState);
    }

    request->performRequest = [this, request]()
    {
        return performRequest_backupPut(request);
    };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::fetchAds(int adFlags, MegaStringList *adUnits, MegaHandle publicHandle, MegaRequestListener *listener)
{
    MegaRequestPrivate* request = new MegaRequestPrivate(MegaRequest::TYPE_FETCH_ADS, listener);
    request->setNumber(adFlags);
    request->setMegaStringList(adUnits);
    request->setNodeHandle(publicHandle);

    request->performRequest = [this, request]()
    {
        int flags = int(request->getNumber());
        MegaStringListPrivate* ads = static_cast<MegaStringListPrivate*>(request->getMegaStringList());
        if (flags < MegaApi::ADS_DEFAULT || flags > MegaApi::ADS_FLAG_IGNORE_ROLLOUT ||
            !ads || !ads->size())
        {
            return API_EARGS;
        }

        client->queueCommand(new CommandFetchAds(
            client,
            flags,
            ads->getVector(),
            request->getNodeHandle(),
            [request, this](Error e, string_map value)
            {
                if (e == API_OK)
                {
                    std::unique_ptr<MegaStringMap> stringMap =
                        std::unique_ptr<MegaStringMap>(MegaStringMap::createInstance());
                    for (const auto& itMap: value)
                    {
                        stringMap->set(itMap.first.c_str(), itMap.second.c_str());
                    }

                    request->setMegaStringMap(stringMap.get());
                }

                fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e));
            }));

        return API_OK;
    };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::queryAds(int adFlags, MegaHandle publicHandle, MegaRequestListener *listener)
{
    MegaRequestPrivate* request = new MegaRequestPrivate(MegaRequest::TYPE_QUERY_ADS, listener);
    request->setNumber(adFlags);
    request->setNodeHandle(publicHandle);

    request->performRequest = [this, request]()
    {
        int flags = int(request->getNumber());
        if (flags < MegaApi::ADS_DEFAULT || flags > MegaApi::ADS_FLAG_IGNORE_ROLLOUT)
        {
            return API_EARGS;
        }

        client->queueCommand(new CommandQueryAds(
            client,
            flags,
            request->getNodeHandle(),
            [request, this](Error e, int value)
            {
                if (e == API_OK)
                    request->setNumDetails(value);
                fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e));
            }));

        return API_OK;
    };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::setCookieSettings(int settings, MegaRequestListener *listener)
{
    MegaRequestPrivate* request = new MegaRequestPrivate(MegaRequest::TYPE_SET_ATTR_USER, listener);
    request->setParamType(MegaApi::USER_ATTR_COOKIE_SETTINGS);
    request->setNumDetails(settings);

    request->performRequest = [this, request]()
    {
        return performRequest_setAttrUser(request);
    };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::setCookieSettings_sendPendingRequests(MegaRequestPrivate* request)
{
    auto tmp = std::to_string(request->getNumDetails());
    client->putua(ATTR_COOKIE_SETTINGS, (byte *)tmp.data(), unsigned(tmp.size()), -1, UNDEF, 0, 0, [this, request](Error e)
    {
        fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e));
    });
}

void MegaApiImpl::getCookieSettings(MegaRequestListener *listener)
{
    MegaRequestPrivate* request = new MegaRequestPrivate(MegaRequest::TYPE_GET_ATTR_USER, listener);
    request->setParamType(MegaApi::USER_ATTR_COOKIE_SETTINGS);

    request->performRequest = [this, request]()
    {
        return performRequest_getAttrUser(request);
    };

    requestQueue.push(request);
    waiter->notify();
}

error MegaApiImpl::getCookieSettings_getua_result(byte* data, unsigned len, MegaRequestPrivate* request)
{
    // make a copy to make sure we don't read too much from a non-null terminated string
    unique_ptr<char[]> buff(new char[len + 1]);
    buff[len] = 0;
    char* startptr = buff.get();
    strncpy(startptr, (const char*)data, len);

    // get the value
    char* endptr;
    long value = strtol(startptr, &endptr, 10);

    // validate the value
    error e = API_OK;
    if (endptr == startptr || *endptr != '\0' || value == LONG_MAX || value == LONG_MIN)
    {
        value = -1;
        LOG_err << "Invalid value for Cookie Settings bitmap";
        e = API_EINTERNAL;
    }

    request->setNumDetails(int(value));

    return e;
}

bool MegaApiImpl::cookieBannerEnabled()
{
    return client->mCookieBannerEnabled;
}

bool MegaApiImpl::startDriveMonitor()
{
    SdkMutexGuard g(sdkMutex);
    return client->startDriveMonitor();
}

void MegaApiImpl::stopDriveMonitor()
{
    SdkMutexGuard g(sdkMutex);
    client->stopDriveMonitor();
}

bool MegaApiImpl::driveMonitorEnabled()
{
    SdkMutexGuard g(sdkMutex);
    return client->driveMonitorEnabled();
}

void MegaApiImpl::enableRequestStatusMonitor(bool enable)
{
    SdkMutexGuard g(sdkMutex);
    enable ? client->startRequestStatusMonitor() : client->stopRequestStatusMonitor();
}

bool MegaApiImpl::requestStatusMonitorEnabled()
{
    SdkMutexGuard g(sdkMutex);
    return client->requestStatusMonitorEnabled();
}

#ifdef USE_DRIVE_NOTIFICATIONS
void MegaApiImpl::drive_presence_changed(bool appeared, const LocalPath& driveRoot)
{
    for (auto it = globalListeners.begin(); it != globalListeners.end(); ++it)
    {
        (*it)->onDrivePresenceChanged(api, appeared, driveRoot.platformEncoded().c_str());
    }
}
#endif

void MegaApiImpl::addMount(const MegaMount* mount, MegaRequestListener* listener)
{
    assert(listener);
    assert(mount);

    auto request = std::make_unique<MegaRequestPrivate>(MegaRequest::TYPE_ADD_MOUNT);

    request->setFile(mount->getPath());
    request->setListener(listener);

    auto execute = [this](const fuse::MountInfo& info, MegaRequestPrivate& request) {
        auto result = client->mFuseService.add(info);
        auto error = std::make_unique<MegaErrorPrivate>(result);

        fireOnRequestFinish(&request, std::move(error));

        return API_OK;
    };

    request->performRequest =
      std::bind(std::move(execute),
                static_cast<const MegaMountPrivate*>(mount)->asInfo(),
                std::ref(*request));

    requestQueue.push(std::move(request));

    waiter->notify();
}

void MegaApiImpl::disableMount(const char* name,
                               MegaRequestListener* listener,
                               bool remember)
{
    assert(listener);
    assert(name);

    auto request = std::make_unique<MegaRequestPrivate>(MegaRequest::TYPE_DISABLE_MOUNT);

    request->setFile(name);
    request->setListener(listener);

    auto execute = [remember, this](MegaRequestPrivate& request) {
        using namespace fuse;

        auto callback = [this](MegaRequestPrivate& request, MountResult result) {
            auto error = std::make_unique<MegaErrorPrivate>(result);

            fireOnRequestFinish(&request, std::move(error));
        };

        std::string name = request.getFile();

        client->mFuseService.disable(std::bind(std::move(callback),
                                               std::ref(request),
                                               std::placeholders::_1),
                                     name,
                                     remember);

        return API_OK;
    };

    request->performRequest =
      std::bind(std::move(execute), std::ref(*request));

    requestQueue.push(std::move(request));

    waiter->notify();
}

void MegaApiImpl::enableMount(const char* name,
                              MegaRequestListener* listener,
                              bool remember)
{
    assert(listener);
    assert(name);

    auto request = std::make_unique<MegaRequestPrivate>(MegaRequest::TYPE_ENABLE_MOUNT);

    request->setFile(name);
    request->setListener(listener);

    auto execute = [remember, this](MegaRequestPrivate& request) {
        auto name = std::string(request.getFile());
        auto result = client->mFuseService.enable(name, remember);
        auto error = std::make_unique<MegaErrorPrivate>(result);

        fireOnRequestFinish(&request, std::move(error));

        return API_OK;
    };

    request->performRequest =
      std::bind(std::move(execute), std::ref(*request));

    requestQueue.push(std::move(request));

    waiter->notify();
}

void MegaApiImpl::fireOnFuseEvent(FuseEventHandler handler,
                                  const fuse::MountEvent& event)
{
    // No listeners? No need to translate the path.
    if (listeners.empty())
        return;

    // Latch path and result.
    auto result = static_cast<int>(event.mResult);

    // Signal listeners.
    for (auto* listener : listeners)
        (listener->*handler)(api, event.mName.c_str(), result);
}

MegaMountFlags* MegaApiImpl::getMountFlags(const char* name)
{
    SdkMutexGuard guard(sdkMutex);

    assert(name);

    auto flags = client->mFuseService.flags(name);

    if (flags)
        return new MegaMountFlagsPrivate(*flags);

    return nullptr;
}

MegaMount* MegaApiImpl::getMountInfo(const char* name)
{
    SdkMutexGuard guard(sdkMutex);

    assert(name);

    auto info = client->mFuseService.get(std::string(name));

    if (info)
        return new MegaMountPrivate(*info);

    return nullptr;
}

char* MegaApiImpl::getMountPath(const char* name)
{
    SdkMutexGuard guard(sdkMutex);

    if (auto path = client->mFuseService.path(name); !path.empty())
        return MegaApi::strdup(path.toPath(false).c_str());

    return nullptr;
}

MegaMountList* MegaApiImpl::listMounts(bool enabled)
{
    SdkMutexGuard guard(sdkMutex);

    auto mounts = client->mFuseService.get(enabled);

    return new MegaMountListPrivate(std::move(mounts));
}

void MegaApiImpl::onFuseEvent(const fuse::MountEvent& event)
{
    static const FuseEventHandler handlers[] = {
        &MegaListener::onMountAdded,
        &MegaListener::onMountChanged,
        &MegaListener::onMountDisabled,
        &MegaListener::onMountEnabled,
        &MegaListener::onMountRemoved
    }; // handlers

    // Sanity.
    assert(&handlers[event.mType] < std::end(handlers));

    // Broadcast the event.
    fireOnFuseEvent(handlers[event.mType], event);
}

bool MegaApiImpl::isCached(const char* path)
{
    assert(path);

    SdkMutexGuard guard(sdkMutex);

    return client->mFuseService.cached(LocalPath::fromPlatformEncodedAbsolute(path));
}

bool MegaApiImpl::isFUSESupported()
{
    SdkMutexGuard guard(sdkMutex);

    return client->mFuseService.supported();
}

bool MegaApiImpl::isMountEnabled(const char* name)
{
    assert(name);

    SdkMutexGuard gaurd(sdkMutex);

    return client->mFuseService.enabled(name);
}

void MegaApiImpl::removeMount(const char* name, MegaRequestListener* listener)
{
    assert(listener);
    assert(name);

    auto request = std::make_unique<MegaRequestPrivate>(MegaRequest::TYPE_REMOVE_MOUNT);

    request->setFile(name);
    request->setListener(listener);

    auto execute = [this](MegaRequestPrivate& request) {
        auto name = std::string(request.getFile());
        auto result = client->mFuseService.remove(name);
        auto error = std::make_unique<MegaErrorPrivate>(result);

        fireOnRequestFinish(&request, std::move(error));

        return API_OK;
    };

    request->performRequest =
      std::bind(std::move(execute), std::ref(*request));

    requestQueue.push(std::move(request));

    waiter->notify();
}

void MegaApiImpl::setFUSEFlags(const MegaFuseFlags& flags)
{
    SdkMutexGuard guard(sdkMutex);

    auto& flags_ = reinterpret_cast<const MegaFuseFlagsPrivate&>(flags);

    client->mFuseService.serviceFlags(flags_.getFlags());
}

MegaFuseFlags* MegaApiImpl::getFUSEFlags()
{
    SdkMutexGuard guard(sdkMutex);

    return new MegaFuseFlagsPrivate(client->mFuseService.serviceFlags());
}

void MegaApiImpl::setMountFlags(const MegaMountFlags* flags,
                                const char* name,
                                MegaRequestListener* listener)
{
    assert(flags);
    assert(listener);
    assert(name);

    auto request = std::make_unique<MegaRequestPrivate>(MegaRequest::TYPE_SET_MOUNT_FLAGS);

    request->setFile(name);
    request->setListener(listener);

    auto execute = [this](const fuse::MountFlags& flags,
                          MegaRequestPrivate& request) {
        auto name = std::string(request.getFile());
        auto result = client->mFuseService.flags(name, flags);
        auto error = std::make_unique<MegaErrorPrivate>(result);

        fireOnRequestFinish(&request, std::move(error));

        return API_OK;
    };

    request->performRequest =
      std::bind(std::move(execute),
                static_cast<const MegaMountFlagsPrivate*>(flags)->getFlags(),
                std::ref(*request));

    requestQueue.push(std::move(request));

    waiter->notify();
}

//
// Sets and Elements
//

void MegaApiImpl::putSetElements(MegaHandle sid, const MegaHandleList* nodes, const MegaStringList* names, MegaRequestListener* listener)
{
    assert(nodes && nodes->size() && (!names || names->size() == static_cast<int>(nodes->size())));
    MegaRequestPrivate* request = new MegaRequestPrivate(MegaRequest::TYPE_PUT_SET_ELEMENTS, listener);
    request->setTotalBytes(static_cast<long long>(sid));
    request->setMegaHandleList(nodes);
    request->setMegaStringList(names);

    request->performRequest = [this, request]()
    {
        MegaHandleList* nodes = request->getMegaHandleList();
        MegaStringList* names = request->getMegaStringList();
        vector<SetElement> els(nodes->size());
        for (size_t i = 0u; i < els.size(); ++i)
        {
            SetElement& el = els[i];
            el.setSet(static_cast<handle>(request->getTotalBytes()));
            el.setNode(nodes->get(static_cast<unsigned int>(i)));
            if (names)
            {
                el.setName(names->get(static_cast<int>(i)));
            }
        }

        client->putSetElements(std::move(els), [this, request](Error e, const vector<const SetElement*>* retEls, const vector<int64_t>* elErrs)
            {
                if (e == API_OK)
                {
                    if (retEls)
                    {
                        request->setMegaSetElementList(std::make_unique<MegaSetElementListPrivate>(retEls->data(), static_cast<int>(retEls->size())));
                    }
                    if (elErrs)
                    {
                        request->setMegaIntegerList(std::make_unique<MegaIntegerListPrivate>(*elErrs));
                    }
                }
                fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e));
            });

        return API_OK;
    };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::removeSetElements(MegaHandle sid, const MegaHandleList* eids, MegaRequestListener* listener)
{
    MegaRequestPrivate* request = new MegaRequestPrivate(MegaRequest::TYPE_REMOVE_SET_ELEMENTS, listener);
    request->setTotalBytes(static_cast<long long>(sid));
    request->setMegaHandleList(eids);

    request->performRequest = [this, request]()
    {
        const MegaHandleList* eidsList = request->getMegaHandleList();
        if (!eidsList)
        {
            return API_ENOENT;
        }

        std::vector<handle> eids(eidsList->size());
        for (size_t i = 0u; i < eids.size(); ++i)
        {
            eids[i] = eidsList->get(static_cast<unsigned int>(i));
        }

        client->removeSetElements(
            static_cast<handle>(request->getTotalBytes()),
            std::move(eids),
            [this, request](Error e, const vector<int64_t>* elErrs)
            {
                if (e == API_OK && elErrs)
                {
                    request->setMegaIntegerList(std::make_unique<MegaIntegerListPrivate>(*elErrs));
                }
                fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e));
            });

        return API_OK;
    };

    requestQueue.push(request);
    waiter->notify();
}

MegaSetList* MegaApiImpl::getSets()
{
    SdkMutexGuard g(sdkMutex);

    MegaSetListPrivate* sList = new MegaSetListPrivate(client->getSets());

    return sList;
}

MegaSet* MegaApiImpl::getSet(MegaHandle sid)
{
    SdkMutexGuard g(sdkMutex);

    const Set* s = client->getSet(sid);
    return s ? (new MegaSetPrivate(*s)) : nullptr;
}

MegaHandle MegaApiImpl::getSetCover(MegaHandle sid)
{
    SdkMutexGuard g(sdkMutex);

    const Set* s = client->getSet(sid);
    return s ? s->cover() : INVALID_HANDLE;
}

bool MegaApiImpl::nodeInRubbishCheck(handle h) const
{
    std::shared_ptr<Node> n = client->nodebyhandle(h);
    bool inRubbish = n && n->firstancestor()->type == RUBBISHNODE;
    return inRubbish;
}

unsigned MegaApiImpl::getSetElementCount(MegaHandle sid, bool includeElementsInRubbishBin)
{
    SdkMutexGuard g(sdkMutex);

    if (includeElementsInRubbishBin)
    {
        return client->getSetElementCount(sid);
    }
    else
    {
        auto els = client->getSetElements(sid);
        auto ret = count_if(begin(*els), end(*els), [this](const pair<const handle, SetElement>& e)
                            { return !nodeInRubbishCheck(e.second.node()); });
        return static_cast<unsigned>(ret);
    }
}

MegaSetElementList* MegaApiImpl::getSetElements(MegaHandle sid, bool includeElementsInRubbishBin)
{
    SdkMutexGuard g(sdkMutex);

    auto* elements = client->getSetElements(sid);
    std::function<bool(handle)> filterOut;
    if (!includeElementsInRubbishBin)
    {
        filterOut = std::bind(&MegaApiImpl::nodeInRubbishCheck, this, std::placeholders::_1);
    }

    MegaSetElementListPrivate* eList = new MegaSetElementListPrivate(elements, filterOut);
    return eList;
}

MegaSetElement* MegaApiImpl::getSetElement(MegaHandle sid, MegaHandle eid)
{
    SdkMutexGuard g(sdkMutex);

    const SetElement* el = client->getSetElement(sid, eid);

    return el ? (new MegaSetElementPrivate(*el)) : nullptr;
}

MegaSetListPrivate::MegaSetListPrivate(const Set *const* sets, int count)
{
    if (sets && count)
    {
        mSets.reserve(static_cast<size_t>(count));
        for (int i = 0; i < count; ++i)
        {
            const Set& s = *sets[i];
            add(MegaSetPrivate(s));
        }
    }
}

MegaSetListPrivate::MegaSetListPrivate(const map<handle, Set>& sets)
{
    mSets.reserve(sets.size());
    for (const auto& sp : sets)
    {
        const Set& s = sp.second;
        add(MegaSetPrivate(s));
    }
}

void MegaSetListPrivate::add(MegaSetPrivate&& s)
{
    mSets.emplace_back(std::move(s));
}


MegaSetElementListPrivate::MegaSetElementListPrivate(const SetElement* const* elements, int count)
{
    if (elements && count)
    {
        mElements.reserve(static_cast<size_t>(count));
        for (int i = 0; i < count; ++i)
        {
            const SetElement& el = *elements[i];
            add(MegaSetElementPrivate(el));
        }
    }
}

MegaSetElementListPrivate::MegaSetElementListPrivate(const elementsmap_t* elements, const std::function<bool(handle)>& filterOut)
{
    if (elements)
    {
        mElements.reserve(elements->size());
        for (const auto& e : *elements)
        {
            if (!filterOut || !filterOut(e.second.node()))
            {
                const SetElement& el = e.second;
                add(MegaSetElementPrivate(el));
            }
        }
        mElements.shrink_to_fit();
    }
}

void MegaSetElementListPrivate::add(MegaSetElementPrivate&& el)
{
    mElements.emplace_back(std::move(el));
}

bool MegaApiImpl::isExportedSet(MegaHandle sid)
{
    SdkMutexGuard g(sdkMutex);

    return client->isExportedSet(sid);
}

void MegaApiImpl::exportSet(MegaHandle sid, bool create, MegaRequestListener* listener)
{
    MegaRequestPrivate* request = new MegaRequestPrivate(MegaRequest::TYPE_EXPORT_SET, listener);
    request->setNodeHandle(sid);
    request->setFlag(create);
    request->performRequest = [this, request]()
    {
        client->exportSet(request->getNodeHandle(), request->getFlag(), [this, request](Error e)
        {
            if (e == API_OK)
            {
                const bool isExport = request->getFlag();
                const auto sid = request->getNodeHandle();
                const Set* updatedSet = client->getSet(sid);
                if (!updatedSet)
                {
                    LOG_err << "Sets: Set to be updated not found for " << (isExport ? "en" : "dis")
                            << "able export operation. Set id " << toHandle(sid);
                    assert(false);
                }
                if((isExport && !updatedSet->isExported())
                   || (!isExport && updatedSet->isExported()))
                {
                    LOG_err << "Sets: Set " << (isExport ? "en" : "dis") << "able operation with"
                            << " incoherent result state isExported()==" << updatedSet->isExported()
                            << ". Set id " << toHandle(sid);
                    assert(false);
                }

                string url;
                if (isExport)
                {
                    std::tie(e, url) = client->getPublicSetLink(updatedSet->id());
                }
                if (e == API_OK)
                {
                    request->setLink(url.c_str());
                    request->setMegaSet(unique_ptr<MegaSet>(new MegaSetPrivate(*updatedSet)));
                    unique_ptr<MegaSetListPrivate> updatedSetList(new MegaSetListPrivate(&updatedSet, 1));
                    fireOnSetsUpdate(updatedSetList.get());
                }
            }
            fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e));
        });

        return API_OK;
    };
    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::exportSet(MegaHandle sid, MegaRequestListener* listener)
{
    exportSet(sid, true, listener);
}

void MegaApiImpl::disableExportSet(MegaHandle sid, MegaRequestListener* listener)
{
    exportSet(sid, false, listener);
}

void MegaApiImpl::fetchPublicSet(const char* publicSetLink, MegaRequestListener* listener)
{
    MegaRequestPrivate* request = new MegaRequestPrivate(MegaRequest::TYPE_FETCH_SET, listener);
    request->setLink(publicSetLink);
    request->performRequest = [this, request]() -> ErrorCodes
    {
        const auto e = client->fetchPublicSet(
            request->getLink(),
            [this, request](Error e, Set* s, elementsmap_t* els)
            {
                unique_ptr<Set> sp(s);
                unique_ptr<elementsmap_t> elsp(els);

                if (e == API_OK)
                {
                    assert(sp && elsp);
                    if (sp && elsp)
                    {
                        request->setMegaSet(std::make_unique<MegaSetPrivate>(*sp));
                        request->setMegaSetElementList(std::make_unique<MegaSetElementListPrivate>(elsp.get()));
                    }
                }
                fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e));
            });

        return e;
    };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::stopPublicSetPreview()
{
    SdkMutexGuard g(sdkMutex);

    client->stopSetPreview();
}

bool MegaApiImpl::inPublicSetPreview()
{
    SdkMutexGuard g(sdkMutex);

    return client->inPublicSetPreview();
}

MegaSet* MegaApiImpl::getPublicSetInPreview()
{
    SdkMutexGuard g(sdkMutex);

    const auto s = client->getPreviewSet();

    return s ? new MegaSetPrivate(*s) : nullptr;
}

MegaSetElementList* MegaApiImpl::getPublicSetElementsInPreview()
{
    SdkMutexGuard g(sdkMutex);

    const auto els = client->getPreviewSetElements();

    return els ? new MegaSetElementListPrivate(els) : nullptr;
}

void MegaApiImpl::getPreviewElementNode(MegaHandle eid, MegaRequestListener* listener)
{
    MegaRequestPrivate *request = new MegaRequestPrivate(MegaRequest::TYPE_GET_EXPORTED_SET_ELEMENT, listener);

    request->performRequest = [eid, this, request]()
    {
        const string paramErr = "Error failed to get MegaNode for Set Element " + toHandle(eid) + ". ";
        if (!client->inPublicSetPreview())
        {
            LOG_err << paramErr << "Public Set preview mode disable";
            return API_EACCESS;
        }

        auto element = client->getPreviewSetElement(eid);
        if (!element)
        {
            LOG_err << paramErr << "Element not found in preview mode Set "
                    << toHandle(client->getPreviewSet()->id());
            return API_EARGS;
        }

        auto nm = element->nodeMetadata();
        if (!nm)
        {
            LOG_err << paramErr << "Element node not found for preview";
            return API_ENOENT;
        }

        FileFingerprint ffp;
        m_time_t tm = ffp.unserializefingerprint(&nm->fingerprint) ? ffp.mtime : 0;
        const string megaApiImplFingerprint = MegaNodePrivate::addAppPrefixToFingerprint(nm->fingerprint, nm->s);

        MegaNodePrivate ret(nm->filename.c_str(), FILENODE, nm->s, nm->ts, tm, nm->h, &element->key(), &nm->fa,
                            megaApiImplFingerprint.empty() ? nullptr : megaApiImplFingerprint.c_str(),
                            nullptr, nm->u, INVALID_HANDLE, nullptr, nullptr, false /*isPublic*/, true /*isForeign*/);
        request->setPublicNode(&ret);

        fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(API_OK));
        return API_OK;
    };

    requestQueue.push(request);
    waiter->notify();
}

const char* MegaApiImpl::getPublicLinkForExportedSet(MegaHandle sid)
{
    string retStr;
    error e;
    {
        SdkMutexGuard g(sdkMutex);
        std::tie(e, retStr) = client->getPublicSetLink(sid);
    }

    char* link = nullptr;
    if (e == API_OK)
    {
        auto sz = retStr.size() + 1;
        link = new char[sz];
        std::strncpy(link, retStr.c_str(), sz);
        LOG_verbose << "Successfully created public link " << retStr << "for Set " << toHandle(sid);
    }
    else
    {
        LOG_err << "Failing to create a public link for Set " << toHandle(sid) << " with error code "
                << e << "(" << MegaError::getErrorString(e) << ")";
    }

    return link;
}

/* MegaApiImpl VPN commands BEGIN */
void MegaApiImpl::getVpnRegions(MegaRequestListener* listener)
{
    MegaRequestPrivate* request = new MegaRequestPrivate(MegaRequest::TYPE_GET_VPN_REGIONS, listener);

    request->performRequest = [this, request]()
    {
        client->getVpnRegions(
            [this, request](const Error& e, std::vector<VpnRegion>&& vpnRegions)
            {
                if (e == API_OK)
                {
                    auto vpnRegionsMegaStringList = std::make_unique<MegaStringListPrivate>();
                    for (const auto& r: vpnRegions)
                    {
                        vpnRegionsMegaStringList->add(r.getName().c_str());
                    }
                    request->setMegaStringList(vpnRegionsMegaStringList.get());

                    MegaVpnRegionListPrivate* regionsWithDetails =
                        new MegaVpnRegionListPrivate(vpnRegions);
                    request->setMegaVpnRegionsDetailed(regionsWithDetails);
                }

                fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e));
            });
        return API_OK;
    };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::getVpnCredentials(MegaRequestListener* listener)
{
    MegaRequestPrivate* request = new MegaRequestPrivate(MegaRequest::TYPE_GET_VPN_CREDENTIALS, listener);

    request->performRequest = [this, request]()
    {
        client->getVpnCredentials([this, request]
            (const Error& e,
            CommandGetVpnCredentials::MapSlotIDToCredentialInfo&& mapSlotIDToCredentialInfo, /* Map of SlotID: { ClusterID, IPv4, IPv6, DeviceID } */
            CommandGetVpnCredentials::MapClusterPublicKeys&& mapClusterPubKeys, /* Map of ClusterID: Cluster Public Key */
            std::vector<VpnRegion>&& vpnRegions /* VPN Regions */)
            {
                if (e == API_OK && !mapSlotIDToCredentialInfo.empty() && !mapClusterPubKeys.empty() && !vpnRegions.empty())
                {
                    request->setMegaVpnCredentials(
                        new MegaVpnCredentialsPrivate(std::move(mapSlotIDToCredentialInfo),
                                                      std::move(mapClusterPubKeys),
                                                      std::move(vpnRegions)));
                }

                fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e));
            });
        return API_OK;
    };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::putVpnCredential(const char* region, MegaRequestListener* listener)
{
    MegaRequestPrivate* request = new MegaRequestPrivate(MegaRequest::TYPE_PUT_VPN_CREDENTIAL, listener);

    request->setText(region);
    request->performRequest = [this, request]()
    {
        const char* vpnRegion = request->getText();
        if (!vpnRegion || !*vpnRegion)
        {
            LOG_err << "[MegaApiImpl::putVpnCredential] VPN region is EMPTY!";
            return API_EARGS;
        }
        client->putVpnCredential(vpnRegion,
            [this, request]
            (const Error& e, int slotID, std::string&& userPubKey, std::string&& newCredential)
            {
                // SlotIDs are considered valid when greater than 0
                if (e == API_OK && (slotID > 0) && !userPubKey.empty() && !newCredential.empty())
                {
                    request->setNumber(slotID);
                    request->setPassword(userPubKey.c_str());
                    request->setSessionKey(newCredential.c_str());
                }

                fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e));
            });
        return API_OK;
    };

    requestQueue.push(request);
    waiter->notify();
}

/**
 * @brief Helper function specifically introduced to guarantee equivalence between internal and
 * public enums.
 * A new entry added to NetworkConnectivityTestMessageStatus will trigger a compilation warning.
 */
static constexpr int
    toPublicNetworkConnectivityTestStatus(const NetworkConnectivityTestMessageStatus s)
{
    switch (s)
    {
        case NetworkConnectivityTestMessageStatus::PASS:
            return MegaNetworkConnectivityTestResults::NETWORK_CONNECTIVITY_TEST_PASS;
        case NetworkConnectivityTestMessageStatus::FAIL:
        case NetworkConnectivityTestMessageStatus::NOT_RUN:
            return MegaNetworkConnectivityTestResults::NETWORK_CONNECTIVITY_TEST_FAIL;
        case NetworkConnectivityTestMessageStatus::NET_UNREACHABLE:
            return MegaNetworkConnectivityTestResults::NETWORK_CONNECTIVITY_TEST_NET_UNREACHABLE;
    }

    assert(s == NetworkConnectivityTestMessageStatus::FAIL); // should never get here
    return MegaNetworkConnectivityTestResults::NETWORK_CONNECTIVITY_TEST_FAIL;
}

void MegaApiImpl::delVpnCredential(int slotID, MegaRequestListener* listener)
{
    MegaRequestPrivate* request = new MegaRequestPrivate(MegaRequest::TYPE_DEL_VPN_CREDENTIAL, listener);

    request->setNumber(slotID);
    request->performRequest = [this, request]()
    {
        int slotID = static_cast<int>(request->getNumber());
        client->delVpnCredential(slotID,
            [this, request] (const Error& e)
            {
                fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e));
            });
        return API_OK;
    };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::checkVpnCredential(const char* userPubKey, MegaRequestListener* listener)
{
    MegaRequestPrivate* request = new MegaRequestPrivate(MegaRequest::TYPE_CHECK_VPN_CREDENTIAL, listener);

    request->setText(userPubKey);
    request->performRequest = [this, request]()
    {
        const char* userPK = request->getText();
        if (!userPK || !*userPK)
        {
            LOG_err << "[MegaApiImpl::checkVpnCredential] User Public Key is EMPTY!";
            return API_EARGS;
        }
        client->checkVpnCredential(userPK,
            [this, request] (const Error& e)
            {
                fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e));
            });
        return API_OK;
    };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::runNetworkConnectivityTest(MegaRequestListener* listener)
{
    auto* request =
        new MegaRequestPrivate(MegaRequest::TYPE_RUN_NETWORK_CONNECTIVITY_TEST, listener);

    request->performRequest = [this, request]()
    {
        client->runNetworkConnectivityTest(
            [this, request](const Error& e, NetworkConnectivityTestResults&& results)
            {
                if (e == API_OK)
                {
                    client->sendNetworkConnectivityTestEvent(results);

                    request->setMegaNetworkConnectivityTestResults(
                        new MegaNetworkConnectivityTestResultsPrivate(
                            toPublicNetworkConnectivityTestStatus(results.ipv4.messages),
                            toPublicNetworkConnectivityTestStatus(results.ipv4.dns),
                            toPublicNetworkConnectivityTestStatus(results.ipv6.messages),
                            toPublicNetworkConnectivityTestStatus(results.ipv6.dns)));
                }
                fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e));
            });
        return API_OK;
    };

    requestQueue.push(request);
    waiter->notify();
}

/* MegaApiImpl VPN commands END */

void MegaApiImpl::getPasswordManagerBase(MegaRequestListener* listener)
{
    MegaRequestPrivate* request = new MegaRequestPrivate(MegaRequest::TYPE_CREATE_PASSWORD_MANAGER_BASE, listener);

    request->performRequest = [this, request]()
    {
        // 1. Shortcut: if already present, nothing to be done
        if (!client->getPasswordManagerBase().isUndef())
        {
            request->setNodeHandle(client->getPasswordManagerBase().as8byte());
            fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(API_OK));
            return API_OK;
        }

        // 2. Check node existance (pwmh user attribute)
        CommandGetUA::CompletionErr ce = [this, request](error e) -> void
        {
            LOG_err << "Password Manager: pwmh user attribute request failed unexpectedly with "
                    << "error " << e << ". Finishing request";
            fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e));
        };
        CommandGetUA::CompletionBytes cb =
            [this, request](byte* /*data*/, unsigned /*len*/, attr_t /*type*/) -> void
        {
            request->setNodeHandle(client->getPasswordManagerBase().as8byte());
            assert(!ISUNDEF(request->getNodeHandle()));
            fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(API_OK));
        };
        CommandGetUA::CompletionTLV ctlv = [this, request](unique_ptr<string_map>, attr_t) -> void
        {
            LOG_err << "Password Manager: ERROR CompletionTLV callback evaluated from CommandGetUA";
            assert(false);

            fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(API_EINTERNAL));
        };
        client->getua(client->finduser(client->me), ATTR_PWM_BASE, request->getTag(),
                      std::move(ce), std::move(cb), std::move(ctlv));
        return API_OK;
    };

    requestQueue.push(request);
    waiter->notify();
}

bool MegaApiImpl::isPasswordManagerNodeFolder(MegaHandle h) const
{
    if (h == UNDEF)
        return false;

    SdkMutexGuard g{sdkMutex};
    const auto n = client->nodebyhandle(h);
    assert(n && "Node not found by handle");
    return n && n->isPasswordManagerNodeFolder();
}

static std::string totpToJson(const MegaNode::PasswordNodeData::TotpData& totp)
{
    if (totp.markedToRemove())
    {
        return "";
    }

    AttrMap attrMap{};

    if (const auto shse = totp.sharedSecret(); shse)
        attrMap.map[AttrMap::string2nameid(MegaClient::PWM_ATTR_PASSWORD_TOTP_SHSE)] = shse;

    if (const auto expirationTime = totp.expirationTime(); expirationTime >= 0)
        attrMap.map[AttrMap::string2nameid(MegaClient::PWM_ATTR_PASSWORD_TOTP_EXPT)] =
            std::to_string(expirationTime);

    if (const auto hashAlgorithm = totp.hashAlgorithm(); hashAlgorithm >= 0)
        attrMap.map[AttrMap::string2nameid(MegaClient::PWM_ATTR_PASSWORD_TOTP_HASH_ALG)] =
            totp::hashAlgorithmPubToStrView(hashAlgorithm);

    if (const auto nDigits = totp.nDigits(); nDigits >= 0)
        attrMap.map[AttrMap::string2nameid(MegaClient::PWM_ATTR_PASSWORD_TOTP_NDIGITS)] =
            std::to_string(nDigits);

    auto result = attrMap.getJsonObject();
    return result;
}

void MegaApiImpl::createPasswordManagerBase(MegaRequestPrivate* request)
{
    CommandCreatePasswordManagerBase::Completion cb = [this, request](Error e, std::unique_ptr<NewNode> nn) -> void
    {
        if (e == API_OK)
        {
            request->setNodeHandle(nn->nodeHandle().as8byte());
        }

        fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e));
    };

    client->createPasswordManagerBase(request->getTag(), std::move(cb));
}

static std::function<void(const std::string_view, const char*)> getAttrSetter(AttrMap& attrMap)
{
    return [&attrMap](const std::string_view key, const char* value)
    {
        if (value)
            attrMap.map[AttrMap::string2nameid(key)] = value;
    };
}

std::unique_ptr<AttrMap>
    MegaApiImpl::toAttrMapCreditCard(const MegaNode::CreditCardNodeData* data) const
{
    if (!data)
        return nullptr;

    auto attrMap = std::make_unique<AttrMap>();
    const auto addAttr = getAttrSetter(*attrMap);
    addAttr(MegaClient::PWM_ATTR_NODE_TYPE,
            std::string{MegaClient::PWM_ATTR_NODE_TYPE_CREDIT_CARD}.c_str());
    addAttr(MegaClient::PWM_ATTR_CREDIT_CARD_NUMBER, data->cardNumber());
    addAttr(MegaClient::PWM_ATTR_CREDIT_NOTES, data->notes());
    addAttr(MegaClient::PWM_ATTR_CREDIT_CARD_HOLDER, data->cardHolderName());
    addAttr(MegaClient::PWM_ATTR_CREDIT_CVV, data->cvv());
    addAttr(MegaClient::PWM_ATTR_CREDIT_EXP_DATE, data->expirationDate());

    return attrMap;
}

std::unique_ptr<AttrMap>
    MegaApiImpl::toAttrMapPassword(const MegaNode::PasswordNodeData* data) const
{
    if (!data)
        return nullptr;

    auto attrMap = std::make_unique<AttrMap>();
    const auto addAttr = getAttrSetter(*attrMap);
    addAttr(MegaClient::PWM_ATTR_PASSWORD_NOTES, data->notes());
    addAttr(MegaClient::PWM_ATTR_PASSWORD_PWD, data->password());
    addAttr(MegaClient::PWM_ATTR_PASSWORD_URL, data->url());
    addAttr(MegaClient::PWM_ATTR_PASSWORD_USERNAME, data->userName());

    if (const auto totp = data->totpData(); totp)
        addAttr(MegaClient::PWM_ATTR_PASSWORD_TOTP, totpToJson(*totp).c_str());

    return attrMap;
}

void MegaApiImpl::createCreditCardNode(const char* name,
                                       const MegaNode::CreditCardNodeData* ccData,
                                       const MegaHandle parentHandle,
                                       MegaRequestListener* listener)
{
    MegaRequestPrivate* request =
        new MegaRequestPrivate(MegaRequest::TYPE_CREATE_PASSWORD_NODE, listener);
    request->setName(name);
    request->setParentHandle(parentHandle);
    request->setParamType(MegaApi::PWM_NODE_TYPE_CREDIT_CARD);

    request->performRequest = [this, request, ccData]() -> error
    {
        auto name = request->getName();
        auto parentHandle = client->nodebyhandle(request->getParentHandle());
        auto data = toAttrMapCreditCard(ccData);

        if (const auto error = checkCreateFolderPrecons(name, parentHandle, request);
            error != API_OK)
            return error;

        // using default this->putnodes_result as callback
        return client->createPasswordEntry(name,
                                           std::move(data),
                                           MegaClient::validateNewCreditCardNodeData,
                                           parentHandle,
                                           request->getTag());
    };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::createPasswordNode(const char* name, const MegaNode::PasswordNodeData* pwData,
                                     MegaHandle parentHandle, MegaRequestListener* listener)
{
    MegaRequestPrivate* request = new MegaRequestPrivate(MegaRequest::TYPE_CREATE_PASSWORD_NODE, listener);
    request->setParentHandle(parentHandle);
    request->setName(name);
    request->setParamType(MegaApi::PWM_NODE_TYPE_PASSWORD);

    request->performRequest = [this, request, pwData]() -> error
    {
        if (pwData && pwData->totpData() && pwData->totpData()->markedToRemove())
        {
            LOG_err << "createPasswordNode: Invalid TotpData with remove arg set to `true`";
            return API_EARGS;
        }

        auto name = request->getName();
        auto parent = client->nodebyhandle(request->getParentHandle());
        auto data = toAttrMapPassword(pwData);
        if (const auto error = checkCreateFolderPrecons(name, parent, request); error != API_OK)
            return error;

        // using default this->putnodes_result as callback
        return client->createPasswordEntry(name,
                                           std::move(data),
                                           MegaClient::validateNewPasswordNodeData,
                                           parent,
                                           request->getTag());
    };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::updateCreditCardNode(MegaHandle node,
                                       const MegaNode::CreditCardNodeData* ccData,
                                       MegaRequestListener* listener)
{
    MegaRequestPrivate* request =
        new MegaRequestPrivate(MegaRequest::TYPE_UPDATE_PASSWORD_NODE, listener);
    request->setNodeHandle(node);
    request->setParamType(MegaApi::PWM_NODE_TYPE_CREDIT_CARD);
    request->performRequest = [this, request, ccData]() -> error
    {
        auto nhPwdNode = NodeHandle{}.set6byte(request->getNodeHandle());
        auto data = toAttrMapCreditCard(ccData);
        CommandSetAttr::Completion cbRequest = [this, request](NodeHandle nh, Error e)
        {
            assert(request->getNodeHandle() == nh.as8byte());
            request->setNodeHandle(nh.as8byte());
            fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e));
        };

        return client->updateCreditCardNode(nhPwdNode, std::move(data), std::move(cbRequest));
    };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::updatePasswordNode(MegaHandle h, const MegaNode::PasswordNodeData* pwData, MegaRequestListener *listener)
{
    MegaRequestPrivate* request = new MegaRequestPrivate(MegaRequest::TYPE_UPDATE_PASSWORD_NODE, listener);
    request->setNodeHandle(h);
    request->setParamType(MegaApi::PWM_NODE_TYPE_PASSWORD);
    request->performRequest = [this, request, pwData]() -> error
    {
        auto nhPwdNode = NodeHandle{}.set6byte(request->getNodeHandle());
        auto data = toAttrMapPassword(pwData);
        CommandSetAttr::Completion cbRequest = [this, request](NodeHandle nh, Error e)
        {
            assert(request->getNodeHandle() == nh.as8byte());
            request->setNodeHandle(nh.as8byte());
            fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e));
        };

        return client->updatePasswordNode(nhPwdNode, std::move(data), std::move(cbRequest));
    };

    requestQueue.push(request);
    waiter->notify();
}

/**
 * @brief Helper function specifically introduced to guarantee equivalence between internal and
 * public enums. If a new entry is added on PasswordEntryError this will trigger a compilation
 * warning.
 */
static constexpr int toPublicErrorCode(const PasswordEntryError e)
{
    switch (e)
    {
        case PasswordEntryError::OK:
            return 0;
        case PasswordEntryError::PARSE_ERROR:
            return MegaApi::IMPORTED_PASSWORD_ERROR_PARSER;
        case PasswordEntryError::MISSING_PASSWORD:
            return MegaApi::IMPORTED_PASSWORD_ERROR_MISSINGPASSWORD;
        case PasswordEntryError::MISSING_NAME:
            return MegaApi::IMPORTED_PASSWORD_ERROR_MISSINGNAME;
        case PasswordEntryError::MISSING_TOTP_SHARED_SECRET:
            return MegaApi::IMPORTED_PASSWORD_ERROR_MISSING_TOTP_SHSE;
        case PasswordEntryError::INVALID_TOTP_SHARED_SECRET:
            return MegaApi::IMPORTED_PASSWORD_ERROR_INVALID_TOTP_SHSE;
        case PasswordEntryError::MISSING_TOTP_NDIGITS:
            return MegaApi::IMPORTED_PASSWORD_ERROR_MISSING_TOTP_NDIGITS;
        case PasswordEntryError::INVALID_TOTP_NDIGITS:
            return MegaApi::IMPORTED_PASSWORD_ERROR_INVALID_TOTP_NDIGITS;
        case PasswordEntryError::MISSING_TOTP_EXPT:
            return MegaApi::IMPORTED_PASSWORD_ERROR_MISSING_TOTP_EXPTIME;
        case PasswordEntryError::INVALID_TOTP_EXPT:
            return MegaApi::IMPORTED_PASSWORD_ERROR_INVALID_TOTP_EXPTIME;
        case PasswordEntryError::MISSING_TOTP_HASH_ALG:
            return MegaApi::IMPORTED_PASSWORD_ERROR_MISSING_TOTP_HASH_ALG;
        case PasswordEntryError::INVALID_TOTP_HASH_ALG:
            return MegaApi::IMPORTED_PASSWORD_ERROR_INVALID_TOTP_HASH_ALG;
        case PasswordEntryError::MISSING_CREDIT_CARD_NUMBER:
            return MegaApi::IMPORTED_PASSWORD_ERROR_MISSING_CREDIT_CARD_NUMBER;
        case PasswordEntryError::INVALID_CREDIT_CARD_NUMBER:
            return MegaApi::IMPORTED_PASSWORD_ERROR_INVALID_CREDIT_CARD_NUMBER;
        case PasswordEntryError::INVALID_CREDIT_CARD_CVV:
            return MegaApi::IMPORTED_PASSWORD_ERROR_INVALID_CREDIT_CARD_CVV;
        case PasswordEntryError::INVALID_CREDIT_CARD_EXPIRATION_DATE:
            return MegaApi::IMPORTED_PASSWORD_ERROR_INVALID_CREDIT_CARD_EXPIRATION_DATE;
    }
    assert(false);
    return -1; // We should never get here
}

void MegaApiImpl::importPasswordsFromFile(const char* filePath,
                                          const int fileSource,
                                          MegaHandle parent,
                                          MegaRequestListener* listener)
{
    MegaRequestPrivate* request =
        new MegaRequestPrivate(MegaRequest::TYPE_IMPORT_PASSWORDS_FROM_FILE, listener);

    request->setFile(filePath);
    request->setParamType(fileSource);
    request->setParentHandle(parent);

    request->performRequest = [this, request]() -> error
    {
        using namespace pwm::import;
        const NodeHandle parentHandle = NodeHandle{}.set6byte(request->getParentHandle());
        if (parentHandle.isUndef() || !request->getFile() ||
            request->getParamType() != MegaApi::IMPORT_PASSWORD_SOURCE_GOOGLE)
        {
            LOG_err << "Import password: invalid parameters";
            return API_EARGS;
        }
        const std::string filePath{request->getFile()};
        const auto source = static_cast<FileSource>(request->getParamType());
        const auto [error, badEntries, nGoodEntries] =
            client->importPasswordsFromFile(filePath, source, parentHandle, request->getTag());
        // Populate request with bad entries
        MegaStringIntegerMapPrivate stringIntegerMap;
        std::for_each(badEntries.begin(),
                      badEntries.end(),
                      [&stringIntegerMap](const std::pair<std::string, PasswordEntryError>& arg)
                      {
                          stringIntegerMap.set(arg.first, toPublicErrorCode(arg.second));
                      });
        request->setMegaStringIntegerMap(&stringIntegerMap);
        if (error == API_OK && nGoodEntries == 0)
        {
            // Special case: All entries are wrong, so there was no call to putnodes
            request->setMegaHandleList(std::vector<handle>{});
            fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(API_OK));
        }
        return error;
    };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::fetchCreditCardInfo(MegaRequestListener* listener)
{
    MegaRequestPrivate* request = new MegaRequestPrivate(MegaRequest::TYPE_FETCH_CREDIT_CARD_INFO, listener);
    request->performRequest = [this, request]()
    {
        client->fetchCreditCardInfo(
            [this, request](Error e, const std::map<std::string, std::string>& creditCardInfo)
            {
                std::unique_ptr<MegaStringMapPrivate> stringMap = std::make_unique<MegaStringMapPrivate>(&creditCardInfo);
                request->setMegaStringMap(stringMap.get());
                fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e));
            });

        return API_OK;
    };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::getVisibleWelcomeDialog(MegaRequestListener* listener)
{
    getUserAttr(nullptr, MegaApi::USER_ATTR_VISIBLE_WELCOME_DIALOG, nullptr, 0, listener);
}

void MegaApiImpl::setVisibleWelcomeDialog(bool visible, MegaRequestListener* listener)
{
    const auto attributeValue{std::to_string(visible)};
    setUserAttr(MegaApi::USER_ATTR_VISIBLE_WELCOME_DIALOG, attributeValue.c_str(), listener);
}

void MegaApiImpl::getVisibleTermsOfService(MegaRequestListener* listener)
{
    getUserAttr(nullptr, MegaApi::USER_ATTR_VISIBLE_TERMS_OF_SERVICE, nullptr, 0, listener);
}

void MegaApiImpl::setVisibleTermsOfService(bool visible, MegaRequestListener* listener)
{
    const string attributeValue{std::to_string(visible)};
    setUserAttr(MegaApi::USER_ATTR_VISIBLE_TERMS_OF_SERVICE, attributeValue.c_str(), listener);
}

void MegaApiImpl::createNodeTree(const MegaNode* parentNode,
                                 MegaNodeTree* nodeTree,
                                 const char* customerIpPort,
                                 MegaRequestListener* listener)
{
    auto request{new MegaRequestPrivate(MegaRequest::TYPE_CREATE_NODE_TREE, listener)};
    request->setParentHandle(parentNode ? parentNode->getHandle() : INVALID_HANDLE);
    request->setMegaNodeTree(nodeTree ? nodeTree->copy() : nullptr);
    request->setText(customerIpPort);
    request->performRequest = [this, request]()
    {
        if (request->getParentHandle() == INVALID_HANDLE || !request->getMegaNodeTree())
        {
            LOG_err << "Failed to create node tree: Missing arguments";
            return API_EARGS;
        }

        // Check for old versions when copying directly to parentNode
        // (not to a newly created intermediary folder).
        shared_ptr<Node> ovLocation{ request->getMegaNodeTree()->getNodeTreeChild() ? nullptr : client->nodebyhandle(request->getParentHandle()) };

        std::vector<NewNode> newNodes;

        handle tmpNodeHandle{1};
        handle tmpParentNodeHandle{UNDEF};
        std::unique_ptr<MegaNodeTree> nodeTree(request->getMegaNodeTree()->copy());
        for (auto tmpNodeTree{dynamic_cast<MegaNodeTreePrivate*>(nodeTree.get())}; tmpNodeTree;
             tmpNodeTree = dynamic_cast<MegaNodeTreePrivate*>(tmpNodeTree->getNodeTreeChild()))
        {
            if ((tmpNodeTree->getNodeTreeChild() && tmpNodeTree->getCompleteUploadData()) ||
                (tmpNodeTree->getNodeTreeChild() && tmpNodeTree->getSourceHandle() != INVALID_HANDLE) ||
                (tmpNodeTree->getCompleteUploadData() && tmpNodeTree->getSourceHandle() != INVALID_HANDLE) ||
                tmpNodeTree->getName().empty() )
            {
                LOG_err << "Failed to create node tree: Invalid arguments";
                return API_EARGS;
            }

            if (tmpNodeTree->getSourceHandle() != INVALID_HANDLE) // copy existing node (source)
            {
                shared_ptr<Node> source{ client->nodebyhandle(tmpNodeTree->getSourceHandle()) };

                if (!source || source->type != FILENODE)
                {
                    LOG_err << "Failed to create node tree: Source was not a file";
                    return API_EARGS;
                }

                vector<NewNode> treeToCopy;
                error err = copyTreeFromOwnedNode(source,
                                                  tmpNodeTree->getName().c_str(),
                                                  ovLocation,
                                                  treeToCopy,
                                                  tmpNodeTree->getS4AttributeValue());
                if (err != API_OK)
                {
                    if (err == API_EEXIST) // dedicated error code when that exact same file already existed
                    {
                        assert(!treeToCopy.empty()); // never empty because other error should have been reported in that case
                        tmpNodeTree->setNodeHandle(treeToCopy[0].ovhandle.as8byte());
                        request->setNodeHandle(tmpNodeTree->getNodeHandle());
                        request->setMegaNodeTree(nodeTree.release());
                        fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(API_OK));
                        return API_OK;
                    }

                    return err;
                }

                assert(!treeToCopy.empty()); // never empty because an error should have been reported in that case
                treeToCopy[0].parenthandle = tmpParentNodeHandle;

                newNodes.emplace_back(std::move(treeToCopy[0]));
            }

            else // complete upload
            {
                const auto completeUploadData{ dynamic_cast<const MegaCompleteUploadDataPrivate*>(
                    tmpNodeTree->getCompleteUploadData()) };

                NewNode newNode{};
                newNode.source = completeUploadData ? NEW_UPLOAD : NEW_NODE;
                newNode.type = completeUploadData ? FILENODE : FOLDERNODE;
                newNode.nodehandle = tmpNodeHandle++;
                newNode.parenthandle = tmpParentNodeHandle;
                newNode.fileattributes.clear();
                tmpParentNodeHandle = newNode.nodehandle;

                // Normalize name
                std::string name{ tmpNodeTree->getName() };
                LocalPath::utf8_normalize(&name);

                // Set node key
                if (completeUploadData)
                {
                    // determine handling of older versions
                    if (std::shared_ptr<Node> ovn = ovLocation ? client->getovnode(ovLocation.get(), &name) : nullptr)
                    {
                        auto ovfp = ovn->attrs.map.find('c');
                        if (ovfp != ovn->attrs.map.end() &&
                            ovfp->second == MegaNodePrivate::removeAppPrefixFromFingerprint(completeUploadData->getFingerprint().c_str()))
                        {
                            tmpNodeTree->setNodeHandle(ovn->nodeHandle().as8byte());
                            request->setNodeHandle(tmpNodeTree->getNodeHandle());
                            request->setMegaNodeTree(nodeTree.release());
                            fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(API_OK));
                            return API_OK;
                        }

                        newNode.ovhandle = ovn->nodeHandle();
                    }

                    byte* nodeKey;
                    size_t nodeKeyLength{ FILENODEKEYLENGTH };
                    base64ToBinary(completeUploadData->getString64FileKey().c_str(),
                        &nodeKey,
                        &nodeKeyLength);
                    newNode.nodekey.assign(reinterpret_cast<char*>(nodeKey), nodeKeyLength);
                    delete[] nodeKey;
                    SymmCipher::xorblock(
                        reinterpret_cast<const byte*>(newNode.nodekey.data()) + SymmCipher::KEYLENGTH,
                        const_cast<byte*>(reinterpret_cast<const byte*>(newNode.nodekey.data())));
                }
                else
                {
                    constexpr size_t nodeKeyLength{ FOLDERNODEKEYLENGTH };
                    byte nodeKey[nodeKeyLength];
                    client->rng.genblock(nodeKey, nodeKeyLength);
                    newNode.nodekey.assign(reinterpret_cast<char*>(nodeKey), nodeKeyLength);
                }

                // Set name
                AttrMap attributes;
                attributes.map['n'] = name;

                // Set S4 attribute
                attributes.map[AttrMap::string2nameid("s4")] = tmpNodeTree->getS4AttributeValue();

                // Set fingerprint
                if (completeUploadData)
                {
                    attributes.map['c'] = MegaNodePrivate::removeAppPrefixFromFingerprint(completeUploadData->getFingerprint().c_str());
                }

                // Set attributes
                std::string attrstring;
                attributes.getjson(&attrstring);
                newNode.attrstring.reset(new std::string);

                SymmCipher cipher;
                cipher.setkey(&newNode.nodekey);
                client->makeattr(&cipher, newNode.attrstring, attrstring.c_str());

                // Set upload
                if (completeUploadData)
                {
                    UploadToken uploadToken{};
                    const size_t uploadTokenSize{ static_cast<size_t>(
                        Base64::atob(completeUploadData->getString64UploadToken().c_str(),
                                     &uploadToken[0],
                                     sizeof(uploadToken))) };
                    if ((uploadTokenSize != UPLOADTOKENLEN) || (uploadTokenSize != sizeof(uploadToken)))
                    {
                        return API_EARGS;
                    }
                    newNode.uploadtoken = uploadToken;
                    newNode.uploadhandle = client->mUploadHandle.next();
                }

                newNodes.push_back(std::move(newNode));
            }
        }

        auto result{[this, request, nodeTree = std::shared_ptr<MegaNodeTree>(nodeTree.release())](
                        const Error& error,
                        targettype_t,
                        vector<NewNode>& newNodes,
                        bool,
                        int,
                        const map<string, string>& fileHandles)
                    {
                        size_t i{};
                        auto tmpNodeTree{dynamic_cast<MegaNodeTreePrivate*>(nodeTree.get())};
                        while (i < newNodes.size() && tmpNodeTree)
                        {
                            if (auto node{client->nodebyhandle(newNodes[i].mAddedHandle)})
                            {
                                tmpNodeTree->setNodeHandle(node->nodehandle);
                            }
                            i++;
                            tmpNodeTree =
                                dynamic_cast<MegaNodeTreePrivate*>(tmpNodeTree->getNodeTreeChild());
                        }
                        request->setMegaNodeTree(nodeTree->copy());
                        request->setMegaStringMap(fileHandles);
                        fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(error));
                    }};

        client->putnodes(NodeHandle().set6byte(request->getParentHandle()),
                         UseLocalVersioningFlag,
                         std::move(newNodes),
                         nullptr,
                         request->getTag(),
                         false,
                         request->getText() ? request->getText() : string{},
                         result);
        return API_OK;
    };

    requestQueue.push(request);
    waiter->notify();
}

MegaIntegerList* MegaApiImpl::getEnabledNotifications() const
{
    SdkMutexGuard g(sdkMutex);
    return new MegaIntegerListPrivate(client->getEnabledNotifications());
}

void MegaApiImpl::enableTestNotifications(const MegaIntegerList * notificationIds, MegaRequestListener * listener)
{
    MegaRequestPrivate* request = new MegaRequestPrivate(MegaRequest::TYPE_SET_ATTR_USER, listener);
    request->setParamType(MegaApi::USER_ATTR_ENABLE_TEST_NOTIFICATIONS);
    MegaIntegerList* ids = notificationIds ? notificationIds->copy() : nullptr;
    request->setMegaIntegerList(std::unique_ptr<MegaIntegerList>{ ids });

    request->performRequest = [this, request]()
    {
        return performRequest_setAttrUser(request);
    };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::performRequest_enableTestNotifications(MegaRequestPrivate* request)
{
    const auto* ids = request->getMegaIntegerList();
    if (!ids)
    {
        fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(API_EARGS));
        return;
    }

    // Convert MegaIntegerList to CSV format
    string tmp;
    for (int i = 0; i < ids->size(); ++i)
    {
        tmp += std::to_string(ids->get(i)) + ',';
    }

    if (!tmp.empty())
    {
        tmp.pop_back(); // drop trailing ','
    }

    client->putua(ATTR_ENABLE_TEST_NOTIFICATIONS,
        reinterpret_cast<const byte*>(tmp.c_str()), unsigned(tmp.size()),
        -1, UNDEF, 0, 0, [this, request](Error e)
        {
            fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e));
        });
}

void MegaApiImpl::getNotifications(MegaRequestListener* listener)
{
    MegaRequestPrivate* request = new MegaRequestPrivate(MegaRequest::TYPE_GET_NOTIFICATIONS, listener);

    request->performRequest = [this, request]()
    {
        return performRequest_getNotifications(request);
    };

    requestQueue.push(request);
    waiter->notify();
}

error MegaApiImpl::performRequest_getNotifications(MegaRequestPrivate* request)
{
    auto onResult = [this, request](const Error& error, vector<DynamicMessageNotification>&& notifications)
    {
        MegaNotificationList* megaNotifications = new MegaNotificationListPrivate(std::move(notifications));
        request->setMegaNotifications(megaNotifications);
        fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(error));
    };

    client->getNotifications(onResult);

    return API_OK;
}

void MegaApiImpl::setLastReadNotification(uint32_t notificationId, MegaRequestListener* listener)
{
    MegaRequestPrivate* request = new MegaRequestPrivate(MegaRequest::TYPE_SET_ATTR_USER, listener);
    request->setParamType(MegaApi::USER_ATTR_LAST_READ_NOTIFICATION);
    request->setNumber(notificationId);

    request->performRequest = [this, request]()
    {
        return performRequest_setAttrUser(request);
    };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::performRequest_setLastReadNotification(MegaRequestPrivate* request)
{
    const string& tmp = request->getNumber() ? std::to_string(static_cast<uint32_t>(request->getNumber())) : string{};

    client->putua(ATTR_LAST_READ_NOTIFICATION, reinterpret_cast<const byte*>(tmp.c_str()),
        static_cast<unsigned>(tmp.size()), -1, UNDEF, 0, 0,
        [this, request](Error e)
        {
            fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e));
        });
}

void MegaApiImpl::getLastReadNotification(MegaRequestListener* listener)
{
    MegaRequestPrivate* request = new MegaRequestPrivate(MegaRequest::TYPE_GET_ATTR_USER, listener);
    request->setParamType(MegaApi::USER_ATTR_LAST_READ_NOTIFICATION);

    request->performRequest = [this, request]()
    {
        return performRequest_getAttrUser(request);
    };

    requestQueue.push(request);
    waiter->notify();
}

error MegaApiImpl::getLastReadNotification_getua_result(byte* data, unsigned len, MegaRequestPrivate* request)
{
    uint32_t value = 0u;
    error e = API_OK;

    if (len)
    {
        // make a copy to make sure we don't read too much from a non-null terminated stream
        string buff{ reinterpret_cast<char*>(data), len };
        size_t processed = 0;
        value = static_cast<uint32_t>(stoul(buff, &processed));

        // validate the value
        if (processed < buff.size())
        {
            value = 0;
            LOG_err << "Invalid value for Last Read Notification";
            e = API_EINTERNAL;
        }
    }

    request->setNumber(static_cast<long long>(value));

    return e;
}

void MegaApiImpl::setLastActionedBanner(uint32_t notificationId, MegaRequestListener* listener)
{
    MegaRequestPrivate* request = new MegaRequestPrivate(MegaRequest::TYPE_SET_ATTR_USER, listener);
    request->setParamType(MegaApi::USER_ATTR_LAST_ACTIONED_BANNER);
    request->setNumber(notificationId);

    request->performRequest = [this, request]()
    {
        return performRequest_setAttrUser(request);
    };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::performRequest_setLastActionedBanner(MegaRequestPrivate* request)
{
    const string& tmp = request->getNumber() ? std::to_string(static_cast<uint32_t>(request->getNumber())) : string{};

    client->putua(ATTR_LAST_ACTIONED_BANNER, reinterpret_cast<const byte*>(tmp.c_str()),
        static_cast<unsigned>(tmp.size()), -1, UNDEF, 0, 0,
        [this, request](Error e)
        {
            fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e));
        });
}

void MegaApiImpl::getLastActionedBanner(MegaRequestListener* listener)
{
    MegaRequestPrivate* request = new MegaRequestPrivate(MegaRequest::TYPE_GET_ATTR_USER, listener);
    request->setParamType(MegaApi::USER_ATTR_LAST_ACTIONED_BANNER);

    request->performRequest = [this, request]()
    {
        return performRequest_getAttrUser(request);
    };

    requestQueue.push(request);
    waiter->notify();
}

error MegaApiImpl::getLastActionedBanner_getua_result(byte* data, unsigned len, MegaRequestPrivate* request)
{
    uint32_t value = 0u;
    error e = API_OK;

    if (len)
    {
        // make a copy to make sure we don't read too much from a non-null terminated stream
        string buff{ reinterpret_cast<char*>(data), len };
        size_t processed = 0;
        value = static_cast<uint32_t>(stoul(buff, &processed));

        // validate the value
        if (processed < buff.size())
        {
            value = 0;
            LOG_err << "Invalid value for Last Read Notification";
            e = API_EINTERNAL;
        }
    }

    request->setNumber(static_cast<long long>(value));

    return e;
}

void MegaApiImpl::setLastPurgeAcknowledged(int64_t ts, MegaRequestListener* listener)
{
    MegaRequestPrivate* request = new MegaRequestPrivate(MegaRequest::TYPE_SET_ATTR_USER, listener);
    request->setParamType(MegaApi::USER_ATTR_LAST_PURGE_ACKNOWLEDGED);
    request->setNumber(ts);

    request->performRequest = [this, request]()
    {
        return performRequest_setAttrUser(request);
    };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::performRequest_setLastPurgeAcknowledged(MegaRequestPrivate* request)
{
    const string tmp = request->getNumber() ? std::to_string(request->getNumber()) : string{};

    client->putua(ATTR_LAST_PURGE_ACKNOWLEDGED,
                  reinterpret_cast<const byte*>(tmp.c_str()),
                  static_cast<unsigned>(tmp.size()),
                  -1,
                  UNDEF,
                  0,
                  0,
                  [this, request](Error e)
                  {
                      fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e));
                  });
}

void MegaApiImpl::getLastPurgeAcknowledged(MegaRequestListener* listener)
{
    MegaRequestPrivate* request = new MegaRequestPrivate(MegaRequest::TYPE_GET_ATTR_USER, listener);
    request->setParamType(MegaApi::USER_ATTR_LAST_PURGE_ACKNOWLEDGED);

    request->performRequest = [this, request]()
    {
        return performRequest_getAttrUser(request);
    };

    requestQueue.push(request);
    waiter->notify();
}

error MegaApiImpl::getLastPurgeAcknowledged_getua_result(byte* data,
                                                         unsigned len,
                                                         MegaRequestPrivate* request)
{
    int64_t value = 0;
    error e = API_OK;

    if (len)
    {
        string buff{reinterpret_cast<char*>(data), len};
        size_t processed = 0;
        try
        {
            value = static_cast<int64_t>(stoll(buff, &processed));
            if (processed < buff.size())
            {
                value = 0;
                LOG_err << "Invalid value for Last Purge Acknowledged";
                e = API_EINTERNAL;
            }
        }
        catch (...)
        {
            LOG_err << "Invalid value for Last Purge Acknowledged";
            e = API_EINTERNAL;
        }
    }

    request->setNumber(value);
    return e;
}

MegaFlagPrivate* MegaApiImpl::getFlag(const char* flagName,
                                      bool commit,
                                      MegaRequestListener* listener)
{
    std::pair<uint32_t, uint32_t> flag = client->getFlag(flagName);

    if (flag.first == static_cast<decltype(flag.first)>(MegaFlag::FLAG_TYPE_AB_TEST) && commit)
    {
        sendABTestActive(flagName, listener);
    }

    return new MegaFlagPrivate(flag.first, flag.second);
}

void MegaApiImpl::deleteUserAttribute(int type, MegaRequestListener* listener)
{
    MegaRequestPrivate* request = new MegaRequestPrivate(MegaRequest::TYPE_DEL_ATTR_USER, listener);
    request->setParamType(type);

    request->performRequest =
#ifdef DEBUG
        [this, request]()
    {
        attr_t type = static_cast<attr_t>(request->getParamType());
        string attributeName = MegaApiImpl::userAttributeToString(type);
        if (attributeName.empty())
        {
            return API_EARGS;
        }
        client->delua(attributeName.c_str());
        return API_OK;
    };
#else
        []()
    {
        return API_EACCESS;
    };
#endif

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::getActiveSurveyTriggerActions(MegaRequestListener* listener)
{
    MegaRequestPrivate* request =
        new MegaRequestPrivate(MegaRequest::TYPE_GET_ACTIVE_SURVEY_TRIGGER_ACTIONS, listener);

    request->performRequest = [this, request]()
    {
        auto completion = [this, request](const Error& e, const vector<uint32_t>& ids)
        {
            if (e == API_OK)
            {
                request->setMegaIntegerList(std::make_unique<MegaIntegerListPrivate>(ids));
            }

            fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e));
        };

        client->getActiveSurveyTriggerActions(std::move(completion));
        return API_OK;
    };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::answerSurvey(MegaHandle surveyHandle,
                               unsigned int triggerActionId,
                               const char* response,
                               const char* comment,
                               MegaRequestListener* listener)
{
    MegaRequestPrivate* request = new MegaRequestPrivate(MegaRequest::TYPE_ANSWER_SURVEY, listener);
    request->setNodeHandle(surveyHandle);
    request->setParamType(static_cast<int>(triggerActionId));
    request->setText(response);
    request->setFile(comment);
    request->performRequest = [this, request]()
    {
        auto completion = [this, request](const Error& e)
        {
            fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e));
        };

        auto parseRating = [](const char* response) -> std::pair<ErrorCodes, int>
        {
            assert(response);
            int rating{};
            if (auto [ptr, ec] =
                    std::from_chars(response, response + std::strlen(response), rating);
                ec != std::errc{})
            {
                LOG_err
                    << "Request (TYPE_ANSWER_SURVEY). Cannot convert rating into a numeric value: "
                    << rating;
                return {API_EARGS, rating};
            }

            if (rating < 1 || rating > 5)
            {
                LOG_err << "Request (TYPE_ANSWER_SURVEY). Rating value is out of valid range: "
                        << rating;
                return {API_EARGS, rating};
            }

            return {API_OK, rating};
        };

        const unsigned int triggerActionId{static_cast<unsigned int>(request->getParamType())};
        if (triggerActionId < MegaApi::ACT_END_UPLOAD ||
            triggerActionId > MegaApi::ACT_SHARE_FOLDER_FILE)
        {
            LOG_err << "Request (TYPE_ANSWER_SURVEY). triggerActionId value is out of valid range: "
                    << triggerActionId;
            return API_EARGS;
        }

        const char* response{request->getText()};
        const char* comment{request->getFile()};
        if (const auto sendTransferFeedback = triggerActionId == MegaApi::ACT_END_UPLOAD;
            sendTransferFeedback)
        {
            if (!response)
            {
                LOG_err << "Request (TYPE_ANSWER_SURVEY). Invalid response param";
                return API_EARGS;
            }

            // if no comment is provided send empty string comment.
            const std::string c{comment ? comment : ""};
            auto [err, rating] = parseRating(response);
            if (err != API_OK)
            {
                return err;
            }

            sendUserfeedback(rating,
                             c.c_str(),
                             true /*transferFeedback*/,
                             MegaApi::TRANSFER_STATS_UPLOAD);
        }

        CommandAnswerSurvey::Answer answer{request->getNodeHandle(), // survey handle
                                           triggerActionId,
                                           response,
                                           comment};

        client->answerSurvey(answer, std::move(completion));
        return API_OK;
    };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::getSurvey(unsigned int triggerActionId, MegaRequestListener* listener)
{
    MegaRequestPrivate* request = new MegaRequestPrivate(MegaRequest::TYPE_GET_SURVEY, listener);
    request->setParamType(static_cast<int>(triggerActionId));
    request->performRequest = [this, request]()
    {
        auto completion = [this, request](const Error& e, const CommandGetSurvey::Survey& survey)
        {
            if (e == API_OK)
            {
                request->setNodeHandle(survey.h);
                request->setNumDetails(static_cast<int>(survey.maxResponse));
                request->setFile(survey.image.c_str());
                request->setText(survey.content.c_str());
            }

            fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e));
        };

        const auto triggerActionId = static_cast<unsigned int>(request->getParamType());
        client->getSurvey(triggerActionId, std::move(completion));
        return API_OK;
    };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::performRequest_enableTestSurveys(MegaRequestPrivate* request)
{
    const MegaHandleList* ids = request->getMegaHandleList();
    if (!ids)
    {
        fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(API_EARGS));
        return;
    }

    // Example: Join the elements to form "A,B,C"
    const auto joinWithCommaInB64 = [](const MegaHandleList* ids) -> string
    {
        string result;
        for (unsigned i = 0; i < ids->size(); ++i)
        {
            result += string{Base64Str<MegaClient::SURVEYHANDLE>(ids->get(i))} + ',';
        }
        // Remove the ending ","
        if (!result.empty())
            result.pop_back();

        return result;
    };

    const string attributeValue{joinWithCommaInB64(ids)};
    client->putua(ATTR_ENABLE_TEST_SURVEYS,
                  reinterpret_cast<const byte*>(attributeValue.c_str()),
                  unsigned(attributeValue.size()),
                  -1,
                  UNDEF,
                  0,
                  0,
                  [this, request](Error e)
                  {
                      fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e));
                  });
}

#ifdef ENABLE_SYNC
error MegaApiImpl::performRequest_getSyncStalls(MegaRequestPrivate* request)
{
    const auto completion = [this, request](std::unique_ptr<SyncProblems> problems)
    {
        mAddressedStallFilter.removeOldFilters(client->syncs.completedPassCount.load());
        // If the user already addressed some sync issues but the sync hasn't made another pass
        // to generate a new stall list, then the filter will hide those for now
        if (const auto getMap = request->getFlag(); getMap)
        {
            auto stallsMap = std::make_unique<MegaSyncStallMapPrivate>(std::move(*problems),
                                                                       mAddressedStallFilter);
            request->setMegaSyncStallMap(std::move(stallsMap));
        }
        else
        {
            auto stallsList = std::make_unique<MegaSyncStallListPrivate>(std::move(*problems),
                                                                         mAddressedStallFilter);
            request->setMegaSyncStallList(std::move(stallsList));
        }
        fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(API_OK));
    };

    client->syncs.getSyncProblems(std::move(completion), true);
    return API_OK;
}
#endif

void MegaApiImpl::enableTestSurveys(const MegaHandleList* surveyHandles,
                                    MegaRequestListener* listener)
{
    MegaRequestPrivate* request = new MegaRequestPrivate(MegaRequest::TYPE_SET_ATTR_USER, listener);
    request->setParamType(MegaApi::USER_ATTR_ENABLE_TEST_SURVEYS);
    request->setMegaHandleList(surveyHandles);

    request->performRequest = [this, request]()
    {
        return performRequest_setAttrUser(request);
    };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::getMyIp(MegaRequestListener* listener)
{
    MegaRequestPrivate* request = new MegaRequestPrivate(MegaRequest::TYPE_GET_MY_IP, listener);

    request->performRequest = [this, request]()
    {
        client->getMyIp(
            [this, request](const Error& e, string&& countryCode, string&& ipAddress)
            {
                if (!e)
                {
                    request->setName(countryCode.c_str());
                    request->setText(ipAddress.c_str());
                }
                fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e));
            });
        return API_OK;
    };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::getSubscriptionCancellationDetails(const char* originalTransactionId,
                                                     unsigned int gatewayId,
                                                     MegaRequestListener* listener)
{
    MegaRequestPrivate* request =
        new MegaRequestPrivate(MegaRequest::TYPE_GET_SUBSCRIPTION_CANCELLATION_DETAILS, listener);
    request->setText(originalTransactionId);
    request->setNumber(gatewayId);

    request->performRequest = [this, request]()
    {
        const char* transactionId = request->getText();
        unsigned int gateway = static_cast<unsigned int>(request->getNumber());

        client->getSubscriptionCancellationDetails(
            transactionId,
            gateway,
            [this, request](const Error& e,
                            std::string&& originalTransactionId,
                            int expiresDate,
                            int cancelledDate)
            {
                if (!e)
                {
                    request->setText(originalTransactionId.c_str());
                    request->setNumber(static_cast<long long>(expiresDate));
                    request->setNumDetails(cancelledDate);
                }

                fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e));
            });

        return API_OK;
    };

    requestQueue.push(request);
    waiter->notify();
}

void MegaApiImpl::getDiscountCodeInformation(const char* discountCode,
                                             MegaRequestListener* listener)
{
    MegaRequestPrivate* request =
        new MegaRequestPrivate(MegaRequest::TYPE_GET_DISCOUNT_CODE_INFORMATION, listener);
    request->setText(discountCode);

    request->performRequest = [this, request]()
    {
        const char* code = request->getText();
        if (!code)
        {
            LOG_err << "getDiscountCodeInformation: Invalid discount code provided";
            return API_EARGS;
        }

        client->getDiscountCodeInformation(
            code,
            [this, code, request](const Error& e, DiscountCodeInfoExtended&& info)
            {
                if (!e)
                {
                    assert(info.alfanumDiscountCode == code);
                    request->setMegaDiscountCodeInfo(
                        std::make_unique<MegaDiscountCodeInfoPrivate>(std::move(info)));
                }

                fireOnRequestFinish(request, std::make_unique<MegaErrorPrivate>(e));
            });

        return API_OK;
    };

    requestQueue.push(request);
    waiter->notify();
}

MegaFileServiceReclaimOptions* MegaApiImpl::fileServiceGetReclaimOptions()
{
    const auto options = client->mFileService.reclaimOptions();

    return new MegaFileServiceReclaimOptionsPrivate(options);
}

void MegaApiImpl::fileServiceSetReclaimOptions(const MegaFileServiceReclaimOptions* options)
{
    if (!options)
        return;

    auto* optionsImpl = dynamic_cast<const MegaFileServiceReclaimOptionsPrivate*>(options);

    assert(optionsImpl);

    if (!optionsImpl)
        return;

    client->mFileService.reclaimOptions(optionsImpl->getOptions());
}

static file_service::ReclaimOptions
    toReclaimOptions(const MegaFileServiceReclaimOptions* inputOptions)
{
    assert(inputOptions);
    file_service::ReclaimOptions reclaimOptions;
    reclaimOptions.mAgeThreshold = std::chrono::minutes(inputOptions->getAgeThreshold());
    reclaimOptions.mBatchSize = inputOptions->getBatchSize();
    reclaimOptions.mDelay = std::chrono::seconds(inputOptions->getDelay());
    reclaimOptions.mPeriod = std::chrono::seconds(inputOptions->getPeriod());

    if (const auto reclaimThreshold = inputOptions->getReclaimThreshold(); reclaimThreshold >= 0)
        reclaimOptions.mReclaimThreshold = static_cast<uint64_t>(reclaimThreshold);
    else
        reclaimOptions.mReclaimThreshold = std::nullopt;

    reclaimOptions.mReclaimTarget = inputOptions->getReclaimTarget();
    return reclaimOptions;
}

void MegaApiImpl::fileServiceReclaim(const MegaFileServiceReclaimOptions* options,
                                     MegaRequestListener* listener)
{
    auto request =
        std::make_unique<MegaRequestPrivate>(MegaRequest::TYPE_FILE_SERVICE_RECLAIM, listener);

    if (options)
        request->setMegaFileServiceReclaimOptions(options->copy());

    // Reclaim result callback
    auto callback =
        [this, r = request.get()](file_service::FileServiceResultOr<std::uint64_t> result)
    {
        auto error = std::make_unique<MegaErrorPrivate>(result ? API_OK : API_EINTERNAL);
        r->setTotalBytes(static_cast<long long>(result.valueOr(0)));
        this->fireOnRequestFinish(r, std::move(error), true);
    };

    request->performRequest = [this, callback = std::move(callback), request = request.get()]()
    {
        file_service::ReclaimOptions reclaimOptions;
        if (auto* inputOptions = request->getMegaFileServiceReclaimOptions())
        {
            // Use input options
            reclaimOptions = toReclaimOptions(inputOptions);
        }
        else
        {
            // Use current file service options
            reclaimOptions = client->mFileService.reclaimOptions();

            // Set and let reclaim can run immediately by set mReclaimThreshold to 0
            reclaimOptions.mReclaimThreshold = 0;
        }

        // Start a reclaim task
        const auto ret = this->client->mFileService.reclaim(std::move(callback), reclaimOptions);
        // Started successfully or not
        return ret == file_service::FILE_SERVICE_SUCCESS ? API_OK : API_EINTERNAL;
    };

    requestQueue.push(std::move(request));
    waiter->notify();
}

MegaFileServiceStorageInfo*
    MegaApiImpl::fileServiceGetStorageInfo(const MegaFileServiceReclaimOptions* options)
{
    // Default
    file_service::ReclaimOptions* reclaimOptionsPtr = nullptr;

    // There is input options
    file_service::ReclaimOptions reclaimOptions;
    if (options)
    {
        reclaimOptions = toReclaimOptions(options);
        reclaimOptionsPtr = &reclaimOptions;
    }

    const auto storageInfo = client->mFileService.storageInfo(reclaimOptionsPtr);
    if (!storageInfo)
        return nullptr;

    return new MegaFileServiceStorageInfoPrivate(*storageInfo);
}

/* END MEGAAPIIMPL */

int TransferQueue::getLastPushedTag() const
{
    return lastPushedTransferTag;
}

TransferQueue::TransferQueue()
{
}

void TransferQueue::push(MegaTransferPrivate *transfer)
{
    std::lock_guard<std::mutex> g(mutex);
    transfers.push_back(transfer);
    transfer->setPlaceInQueue(++lastPushedTransferTag);
}

void TransferQueue::push_front(MegaTransferPrivate *transfer)
{
    std::lock_guard<std::mutex> g(mutex);
    transfers.push_front(transfer);
}

bool TransferQueue::empty()
{
    std::lock_guard<std::mutex> g(mutex);
    return transfers.empty();
}

size_t TransferQueue::size()
{
    std::lock_guard<std::mutex> g(mutex);
    return transfers.size();
}

void TransferQueue::clear()
{
    std::lock_guard<std::mutex> g(mutex);
    for (auto& t: transfers)
    {
        delete t;
        t = nullptr;
    }
    transfers.clear();
}

MegaTransferPrivate *TransferQueue::pop()
{
    std::lock_guard<std::mutex> g(mutex);
    if(transfers.empty())
    {
        return NULL;
    }
    MegaTransferPrivate *transfer = transfers.front();
    transfers.pop_front();
    return transfer;
}

std::vector<MegaTransferPrivate *> TransferQueue::popUpTo(int lastQueuedTransfer, int direction)
{
    std::lock_guard<std::mutex> g(mutex);
    std::vector<MegaTransferPrivate*> toret;
    for (auto it = transfers.begin(); it != transfers.end();)
    {
        MegaTransferPrivate *transfer = *it;
        if (transfer->getPlaceInQueue() > lastQueuedTransfer)
        {
            break;
        }

        if (!transfer->isSyncTransfer() && transfer->getType() == direction)
        {
            toret.push_back(transfer);
            it = transfers.erase(it);
        }
        else
        {
            it++;
        }
    }
    return toret;
}

void TransferQueue::removeWithFolderTag(int folderTag, std::function<void(MegaTransferPrivate *)> callback)
{
    // We need to lock the TransferQueue's mutex or it's not safe to iterate transfers.
    // However this is risky because we are making callbacks with it locked
    // We shouldn't cause a deadlock wih the impl mutex because that one is always locked before calling here.
    // However the callback (including its calls to fireOnXYZ() ) must be careful not to lock any mutex which
    // may have been locked during other MegaApi function calls.
    std::lock_guard<std::mutex> g(mutex);

    for (auto it = transfers.begin(); it != transfers.end();)
    {
        if ((*it)->getFolderTransferTag() == folderTag)
        {
            if (callback)
            {
                callback(*it);
            }
            it = transfers.erase(it);
        }
        else
        {
            it++;
        }
    }
}

void TransferQueue::removeListener(MegaTransferListener *listener)
{
    std::lock_guard<std::mutex> g(mutex);

    std::deque<MegaTransferPrivate *>::iterator it = transfers.begin();
    while(it != transfers.end())
    {
        MegaTransferPrivate *transfer = (*it);
        if(transfer->getListener() == listener)
            transfer->setListener(NULL);
        it++;
    }
}

void TransferQueue::setAllCancelled(CancelToken cancelled, int direction)
{
    std::lock_guard<std::mutex> g(mutex);
    for (auto& t : transfers)
    {
        if (t->getType() == direction
            && !t->isSyncTransfer()
            && !t->isStreamingTransfer())
        {
            t->setCancelToken(cancelled);
        }
    }
}

void RequestQueue::RetainedRequest::removeListener(MegaRequestListener* listener)
{
    if (mRequest && mRequest->getListener() == listener)
        mRequest->setListener(NULL);
}

void RequestQueue::RetainedRequest::removeListener(MegaScheduledCopyListener* listener)
{
    if (mRequest && mRequest->getBackupListener() == listener)
        mRequest->setBackupListener(NULL);
}

RequestQueue::ScopedRetainingRequest::ScopedRetainingRequest(
    RequestQueue::RetainedRequest& retainedRequest,
    MegaRequestPrivate* request):
    mRetainedRequest{retainedRequest}
{
    mRetainedRequest.set(request);
}

RequestQueue::ScopedRetainingRequest::~ScopedRetainingRequest()
{
    mRetainedRequest.clear();
}

RequestQueue::RequestQueue()
{
}

std::unique_ptr<RequestQueue::ScopedRetainingRequest>
    RequestQueue::scopedRetainingRequest(MegaRequestPrivate* request)
{
    std::lock_guard<std::mutex> guard(mutex);
    return std::make_unique<RequestQueue::ScopedRetainingRequest>(this->retainedRequest, request);
}

void RequestQueue::push(MegaRequestPrivate *request)
{
    std::lock_guard<std::mutex> g(mutex);
    requests.push_back(request);
}

void RequestQueue::push(std::unique_ptr<MegaRequestPrivate> request)
{
    std::lock_guard<std::mutex> guard(mutex);

    requests.push_back(request.get());

    request.release();
}

void RequestQueue::push_front(MegaRequestPrivate *request)
{
    std::lock_guard<std::mutex> g(mutex);
    requests.push_front(request);
}

MegaRequestPrivate *RequestQueue::pop()
{
    std::lock_guard<std::mutex> g(mutex);
    if(requests.empty())
    {
        return NULL;
    }
    MegaRequestPrivate *request = requests.front();
    requests.pop_front();
    return request;
}

MegaRequestPrivate *RequestQueue::front()
{
    std::lock_guard<std::mutex> g(mutex);
    if(requests.empty())
    {
        return NULL;
    }
    MegaRequestPrivate *request = requests.front();
    return request;
}

void RequestQueue::removeListener(MegaRequestListener *listener)
{
    std::lock_guard<std::mutex> g(mutex);

    std::deque<MegaRequestPrivate *>::iterator it = requests.begin();
    while(it != requests.end())
    {
        MegaRequestPrivate *request = (*it);
        if(request->getListener()==listener)
            request->setListener(NULL);
        it++;
    }

    retainedRequest.removeListener(listener);
}

void RequestQueue::removeListener(MegaScheduledCopyListener *listener)
{
    std::lock_guard<std::mutex> g(mutex);

    std::deque<MegaRequestPrivate *>::iterator it = requests.begin();
    while(it != requests.end())
    {
        MegaRequestPrivate *request = (*it);
        if(request->getBackupListener()==listener)
            request->setBackupListener(NULL);
        it++;
    }

    retainedRequest.removeListener(listener);
}

MegaHashSignatureImpl::MegaHashSignatureImpl(const char *base64Key)
{
    hashSignature = new HashSignature(new Hash());
    asymmCypher = new AsymmCipher();

    string pubks;
    int len = int(strlen(base64Key)/4*3+3);
    pubks.resize(static_cast<size_t>(len));
    pubks.resize(static_cast<size_t>(Base64::atob(base64Key, (byte*)pubks.data(), len)));
    asymmCypher->setkey(AsymmCipher::PUBKEY,(byte*)pubks.data(), int(pubks.size()));
}

MegaHashSignatureImpl::~MegaHashSignatureImpl()
{
    delete hashSignature;
    delete asymmCypher;
}

void MegaHashSignatureImpl::init()
{
    hashSignature->get(asymmCypher, NULL, 0);
}

void MegaHashSignatureImpl::add(const char *data, unsigned size)
{
    hashSignature->add((const byte *)data, size);
}

bool MegaHashSignatureImpl::checkSignature(const char *base64Signature)
{
    char signature[512];
    int l = Base64::atob(base64Signature, (byte *)signature, sizeof(signature));
    if(l != sizeof(signature))
        return false;

    return hashSignature->checksignature(asymmCypher, (const byte *)signature, sizeof(signature));
}

int MegaAccountDetailsPrivate::getProLevel()
{
    for (const auto& plan: details.plans)
    {
        if (plan.isProPlan())
        {
            return plan.level;
        }
    }

    return MegaAccountDetails::ACCOUNT_TYPE_FREE;
}

int64_t MegaAccountDetailsPrivate::getProExpiration()
{
    return details.pro_until;
}

int MegaAccountDetailsPrivate::getSubscriptionStatus()
{
    // Consider only the first subscription
    if (!details.subscriptions.empty())
    {
        char subType = details.subscriptions.front().type;

        if (subType == 'S')
        {
            return MegaAccountSubscription::SUBSCRIPTION_STATUS_VALID;
        }

        if (subType == 'R')
        {
            return MegaAccountSubscription::SUBSCRIPTION_STATUS_INVALID;
        }
    }

    return MegaAccountSubscription::SUBSCRIPTION_STATUS_NONE;
}

int64_t MegaAccountDetailsPrivate::getSubscriptionRenewTime()
{
    // Consider only the first subscription
    if (!details.subscriptions.empty())
    {
        return details.subscriptions.front().renew;
    }

    return MEGA_INVALID_TIMESTAMP;
}

char *MegaAccountDetailsPrivate::getSubscriptionMethod()
{
    // Consider only the first subscription
    if (!details.subscriptions.empty())
    {
        return MegaApi::strdup(details.subscriptions.front().paymentMethod.c_str());
    }

    return new char[1]{'\0'};
}

int MegaAccountDetailsPrivate::getSubscriptionMethodId()
{
    // Consider only the first subscription
    if (!details.subscriptions.empty())
    {
        return details.subscriptions.front().paymentMethodId;
    }

    return MegaAccountDetails::ACCOUNT_TYPE_FREE;
}

char *MegaAccountDetailsPrivate::getSubscriptionCycle()
{
    // Consider only the first subscription
    if (!details.subscriptions.empty())
    {
        return MegaApi::strdup(details.subscriptions.front().cycle.c_str());
    }

    return new char[1]{'\0'};
}

long long MegaAccountDetailsPrivate::getStorageMax()
{
    return details.storage_max;
}

long long MegaAccountDetailsPrivate::getStorageUsed()
{
    return details.storage_used;
}

long long MegaAccountDetailsPrivate::getVersionStorageUsed()
{
    long long total = 0;

    handlestorage_map::iterator it;
    for (it = details.storage.begin(); it != details.storage.end(); it++)
    {
        total += it->second.version_bytes;
    }

    return total;
}

long long MegaAccountDetailsPrivate::getTransferMax()
{
    return details.transfer_max;
}

long long MegaAccountDetailsPrivate::getTransferOwnUsed()
{
    return details.transfer_own_used;
}

long long MegaAccountDetailsPrivate::getTransferSrvUsed()
{
    return details.transfer_srv_used;
}

long long MegaAccountDetailsPrivate::getTransferUsed()
{
    long long total = details.transfer_srv_used + details.transfer_own_used + getTemporalBandwidth();
    // in case the total exceed the maximum allowance (due to the free IP-based quota)...
    if (details.transfer_max && total > details.transfer_max) //do not limit for free user (no max allowance configured)
    {
        total = details.transfer_max;
    }
    return total;
}

int MegaAccountDetailsPrivate::getNumUsageItems()
{
    return int(details.storage.size());
}

long long MegaAccountDetailsPrivate::getStorageUsed(MegaHandle handle)
{
    handlestorage_map::iterator it = details.storage.find(handle);
    if (it != details.storage.end())
    {
        return it->second.bytes;
    }
    else
    {
        return 0;
    }
}

long long MegaAccountDetailsPrivate::getNumFiles(MegaHandle handle)
{
    handlestorage_map::iterator it = details.storage.find(handle);
    if (it != details.storage.end())
    {
        return it->second.files;
    }
    else
    {
        return 0;
    }
}

long long MegaAccountDetailsPrivate::getNumFolders(MegaHandle handle)
{
    handlestorage_map::iterator it = details.storage.find(handle);
    if (it != details.storage.end())
    {
        return it->second.folders;
    }
    else
    {
        return 0;
    }
}

long long MegaAccountDetailsPrivate::getVersionStorageUsed(MegaHandle handle)
{
    handlestorage_map::iterator it = details.storage.find(handle);
    if (it != details.storage.end())
    {
        return it->second.version_bytes;
    }
    else
    {
        return 0;
    }
}

long long MegaAccountDetailsPrivate::getNumVersionFiles(MegaHandle handle)
{
    handlestorage_map::iterator it = details.storage.find(handle);
    if (it != details.storage.end())
    {
        return it->second.version_files;
    }
    else
    {
        return 0;
    }
}

MegaAccountDetails* MegaAccountDetailsPrivate::copy()
{
    return new MegaAccountDetailsPrivate(&details);
}

int MegaAccountDetailsPrivate::getNumBalances() const
{
    return int(details.balances.size());
}

MegaAccountBalance *MegaAccountDetailsPrivate::getBalance(int i) const
{
    if ((unsigned int)i < details.balances.size())
    {
        return MegaAccountBalancePrivate::fromAccountBalance(&(details.balances[(unsigned int)i]));
    }
    return NULL;
}

int MegaAccountDetailsPrivate::getNumSessions() const
{
    return int(details.sessions.size());
}

MegaAccountSession *MegaAccountDetailsPrivate::getSession(int i) const
{
    if ((unsigned int)i < details.sessions.size())
    {
        return MegaAccountSessionPrivate::fromAccountSession(&(details.sessions[(unsigned int)i]));
    }
    return NULL;
}

int MegaAccountDetailsPrivate::getNumPurchases() const
{
    return int(details.purchases.size());
}

MegaAccountPurchase *MegaAccountDetailsPrivate::getPurchase(int i) const
{
    if ((unsigned int)i < details.purchases.size())
    {
        return MegaAccountPurchasePrivate::fromAccountPurchase(&(details.purchases[(unsigned int)i]));
    }
    return NULL;
}

int MegaAccountDetailsPrivate::getNumTransactions() const
{
    return int(details.transactions.size());
}

MegaAccountTransaction *MegaAccountDetailsPrivate::getTransaction(int i) const
{
    if ((unsigned int)i < details.transactions.size())
    {
        return MegaAccountTransactionPrivate::fromAccountTransaction(&(details.transactions[(unsigned int)i]));
    }
    return NULL;
}

int MegaAccountDetailsPrivate::getTemporalBandwidthInterval()
{
    return int(details.transfer_hist.size());
}

long long MegaAccountDetailsPrivate::getTemporalBandwidth()
{
    long long result = 0;
    for (unsigned int i = 0; i < details.transfer_hist.size(); i++)
    {
        result += details.transfer_hist[i];
    }
    return result;
}

bool MegaAccountDetailsPrivate::isTemporalBandwidthValid()
{
    return details.transfer_hist_valid;
}

int MegaAccountDetailsPrivate::getNumActiveFeatures() const
{
    return static_cast<int>(details.activeFeatures.size());
}

MegaAccountFeature* MegaAccountDetailsPrivate::getActiveFeature(int featureIndex) const
{
    if (static_cast<size_t>(featureIndex) < details.activeFeatures.size())
    {
        return MegaAccountFeaturePrivate::fromAccountFeature(
            &(details.activeFeatures[static_cast<size_t>(featureIndex)]));
    }
    return nullptr;
}

int64_t MegaAccountDetailsPrivate::getSubscriptionLevel() const
{
    // Consider only the first subscription
    if (!details.subscriptions.empty())
    {
        return details.subscriptions.front().level;
    }

    return MegaAccountDetails::ACCOUNT_TYPE_FREE;
}

MegaStringIntegerMap* MegaAccountDetailsPrivate::getSubscriptionFeatures() const
{
    MegaStringIntegerMapPrivate* subscriptionFeatures = new MegaStringIntegerMapPrivate();
    // Consider only the first subscription
    if (!details.subscriptions.empty())
    {
        for (const auto& f: details.subscriptions.front().features)
        {
            // Since all the received features are active, all have a 1.
            // Hardcoded to keep the interface.
            subscriptionFeatures->set(f, 1);
        }
    }

    return subscriptionFeatures;
}

int MegaAccountDetailsPrivate::getNumSubscriptions() const
{
    return static_cast<int>(details.subscriptions.size());
}

MegaAccountSubscription* MegaAccountDetailsPrivate::getSubscription(int featureIndex) const
{
    size_t index = static_cast<size_t>(featureIndex);
    if (index < details.subscriptions.size())
    {
        return MegaAccountSubscriptionPrivate::fromAccountSubscription(
            details.subscriptions[index]);
    }
    return nullptr;
}

int MegaAccountDetailsPrivate::getNumPlans() const
{
    return static_cast<int>(details.plans.size());
}

MegaAccountPlan* MegaAccountDetailsPrivate::getPlan(int featureIndex) const
{
    size_t index = static_cast<size_t>(featureIndex);
    if (index < details.plans.size())
    {
        return MegaAccountPlanPrivate::fromAccountPlan(details.plans[index]);
    }
    return nullptr;
}

MegaErrorPrivate::MegaErrorPrivate(int errorCode)
    : MegaError(errorCode)
{
}

MegaErrorPrivate::MegaErrorPrivate(int errorCode, SyncError syncError)
: MegaError(errorCode, syncError)
{
}
#ifdef ENABLE_SYNC
MegaErrorPrivate::MegaErrorPrivate(int errorCode, MegaSync::Error syncError)
: MegaError(errorCode, syncError)
{
}
#endif

MegaErrorPrivate::MegaErrorPrivate(int errorCode, long long value)
    : MegaError(errorCode)
    , mValue(value)
{
}

MegaErrorPrivate::MegaErrorPrivate(const Error& err)
    : MegaError(err)
    , mValue(0)
    , mUserStatus(err.getUserStatus())
    , mLinkStatus(err.getLinkStatus())
{
}

MegaErrorPrivate::MegaErrorPrivate(fuse::MountResult result)
  : MegaError(result == fuse::MOUNT_SUCCESS ? API_OK : API_EFAILED)
  , mMountResult(result)
{
}

MegaErrorPrivate::MegaErrorPrivate(const MegaError &megaError)
    : MegaError(megaError)
    , mValue(megaError.getValue())
    , mUserStatus(megaError.getUserStatus())
    , mLinkStatus(megaError.getLinkStatus())
{
}

MegaErrorPrivate::~MegaErrorPrivate()
{

}

MegaError* MegaErrorPrivate::copy() const
{
    return new MegaErrorPrivate(*this);
}

int MegaErrorPrivate::getErrorCode() const
{
    return errorCode;
}

int MegaErrorPrivate::getMountResult() const
{
    return static_cast<int>(mMountResult);
}

long long MegaErrorPrivate::getValue() const
{
    return mValue;
}

bool MegaErrorPrivate::hasExtraInfo() const
{
    return mUserStatus != MegaError::UserErrorCode::USER_ETD_UNKNOWN
            || mLinkStatus != MegaError::LinkErrorCode::LINK_UNKNOWN;
}

long long MegaErrorPrivate::getUserStatus() const
{
    return mUserStatus;
}

long long MegaErrorPrivate::getLinkStatus() const
{
    return mLinkStatus;
}

const char* MegaErrorPrivate::getErrorString() const
{
    return MegaError::getErrorString(errorCode);
}

const char* MegaErrorPrivate::toString() const
{
    return getErrorString();
}

MegaPricingPrivate::~MegaPricingPrivate() {}

int MegaPricingPrivate::getNumProducts()
{
    return int(products.size());
}

handle MegaPricingPrivate::getHandle(int productIndex)
{
    if (productIndex >= 0 && static_cast<unsigned int>(productIndex) < products.size())
    {
        return products[static_cast<size_t>(productIndex)].productHandle;
    }

    return UNDEF;
}

int MegaPricingPrivate::getProLevel(int productIndex)
{
    if (productIndex >= 0 && static_cast<unsigned int>(productIndex) < products.size())
    {
        return static_cast<int>(products[static_cast<size_t>(productIndex)].proLevel);
    }

    return 0;
}

int MegaPricingPrivate::getGBStorage(int productIndex)
{
    if (productIndex >= 0 && static_cast<unsigned int>(productIndex) < products.size())
    {
        return products[static_cast<size_t>(productIndex)].gbStorage;
    }

    return 0;
}

int MegaPricingPrivate::getGBTransfer(int productIndex)
{
    if (productIndex >= 0 && static_cast<unsigned int>(productIndex) < products.size())
    {
        return products[static_cast<size_t>(productIndex)].gbTransfer;
    }

    return 0;
}

int MegaPricingPrivate::getMonths(int productIndex)
{
    if (productIndex >= 0 && static_cast<unsigned int>(productIndex) < products.size())
    {
        return static_cast<int>(products[static_cast<size_t>(productIndex)].months);
    }

    return 0;
}

int MegaPricingPrivate::getAmount(int productIndex)
{
    if (productIndex >= 0 && static_cast<unsigned int>(productIndex) < products.size())
    {
        const auto v = products[static_cast<size_t>(productIndex)].amount;
        return static_cast<int>(std::round(v));
    }

    return 0;
}

int mega::MegaPricingPrivate::getLocalPrice(int productIndex)
{
    if (productIndex >= 0 && static_cast<unsigned int>(productIndex) < products.size())
    {
        const auto v = products[static_cast<size_t>(productIndex)].localPrice;
        return static_cast<int>(std::round(v));
    }

    return 0;
}

double mega::MegaPricingPrivate::getAmountWithDecimals(const int productIndex) const
{
    if (productIndex >= 0 && static_cast<unsigned int>(productIndex) < products.size())
    {
        return products[static_cast<size_t>(productIndex)].amount;
    }

    return 0.0;
}

double mega::MegaPricingPrivate::getLocalPriceWithDecimals(const int productIndex) const
{
    if (productIndex >= 0 && static_cast<unsigned int>(productIndex) < products.size())
    {
        return products[static_cast<size_t>(productIndex)].localPrice;
    }

    return 0.0;
}

double mega::MegaPricingPrivate::getAmountMonthWithDecimals(const int productIndex) const
{
    if (productIndex >= 0 && static_cast<unsigned int>(productIndex) < products.size())
    {
        return products[static_cast<size_t>(productIndex)].amountMonth;
    }

    return 0.0;
}

double MegaPricingPrivate::getPriceNetWithDecimals(const int productIndex) const
{
    if (productIndex >= 0 && static_cast<unsigned int>(productIndex) < products.size())
    {
        return products[static_cast<size_t>(productIndex)].priceNet;
    }

    return 0.0;
}

double MegaPricingPrivate::getLocalPriceNetWithDecimals(const int productIndex) const
{
    if (productIndex >= 0 && static_cast<unsigned int>(productIndex) < products.size())
    {
        return products[static_cast<size_t>(productIndex)].localPriceNet;
    }

    return 0.0;
}

double MegaPricingPrivate::getMonthlyBasePriceNetWithDecimals(const int productIndex) const
{
    if (productIndex >= 0 && static_cast<unsigned int>(productIndex) < products.size())
    {
        return products[static_cast<size_t>(productIndex)].monthlyBasePriceNet;
    }

    return 0.0;
}

bool MegaPricingPrivate::hasMobileOffers(int productIndex) const
{
    if (auto index = static_cast<size_t>(productIndex); index < products.size())
    {
        return products[index].mobileOffer.has_value();
    }

    return false;
}

std::string mega::MegaPricingPrivate::getMobileOfferId(int productIndex) const
{
    if (auto index = static_cast<size_t>(productIndex);
        index < products.size() && products[index].mobileOffer.has_value())
    {
        return products[index].mobileOffer->id;
    }

    return {};
}

bool mega::MegaPricingPrivate::hasMobileOfferUat(int productIndex) const
{
    if (auto index = static_cast<size_t>(productIndex);
        index < products.size() && products[index].mobileOffer.has_value())
    {
        return products[index].mobileOffer->uat;
    }

    return {};
}

std::string mega::MegaPricingPrivate::getMobileOfferLabel(int productIndex) const
{
    if (auto index = static_cast<size_t>(productIndex);
        index < products.size() && products[index].mobileOffer.has_value())
    {
        return products[index].mobileOffer->label;
    }

    return {};
}

int mega::MegaPricingPrivate::getMobileOfferDiscountPercentage(int productIndex) const
{
    if (auto index = static_cast<size_t>(productIndex);
        index < products.size() && products[index].mobileOffer.has_value())
    {
        return products[index].mobileOffer->discountPercentage;
    }

    return 0;
}

int64_t MegaPricingPrivate::getMobileOfferExpiryTimestamp(int productIndex) const
{
    if (auto index = static_cast<size_t>(productIndex);
        index < products.size() && products[index].mobileOffer.has_value())
    {
        return products[index].mobileOffer->expiryTimestamp;
    }

    return 0;
}

uint32_t MegaPricingPrivate::getMobileOfferFlags(int productIndex) const
{
    if (auto index = static_cast<size_t>(productIndex);
        index < products.size() && products[index].mobileOffer.has_value())
    {
        return products[index].mobileOffer->flags;
    }

    return 0;
}

int64_t MegaPricingPrivate::getMobileOfferReshowInterval(int productIndex) const
{
    if (auto index = static_cast<size_t>(productIndex);
        index < products.size() && products[index].mobileOffer.has_value())
    {
        return products[index].mobileOffer->reshowInterval;
    }

    return 0;
}

bool MegaPricingPrivate::hasMobileOfferIos(int productIndex) const
{
    if (auto index = static_cast<size_t>(productIndex);
        index < products.size() && products[index].mobileOffer.has_value())
    {
        return products[index].mobileOffer->ios.has_value();
    }

    return false;
}

std::string MegaPricingPrivate::getMobileOfferIosOfferId(int productIndex) const
{
    if (auto index = static_cast<size_t>(productIndex);
        index < products.size() && products[index].mobileOffer.has_value() &&
        products[index].mobileOffer->ios.has_value())
    {
        return products[index].mobileOffer->ios->offerId;
    }

    return {};
}

std::string MegaPricingPrivate::getMobileOfferIosKeyId(int productIndex) const
{
    if (auto index = static_cast<size_t>(productIndex);
        index < products.size() && products[index].mobileOffer.has_value() &&
        products[index].mobileOffer->ios.has_value())
    {
        return products[index].mobileOffer->ios->keyId;
    }

    return {};
}

std::string MegaPricingPrivate::getMobileOfferIosNonce(int productIndex) const
{
    if (auto index = static_cast<size_t>(productIndex);
        index < products.size() && products[index].mobileOffer.has_value() &&
        products[index].mobileOffer->ios.has_value())
    {
        return products[index].mobileOffer->ios->nonce;
    }

    return {};
}

int64_t MegaPricingPrivate::getMobileOfferIosTimestampMs(int productIndex) const
{
    if (auto index = static_cast<size_t>(productIndex);
        index < products.size() && products[index].mobileOffer.has_value() &&
        products[index].mobileOffer->ios.has_value())
    {
        return products[index].mobileOffer->ios->timestampMs;
    }

    return 0;
}

std::string MegaPricingPrivate::getMobileOfferIosSignature(int productIndex) const
{
    if (auto index = static_cast<size_t>(productIndex);
        index < products.size() && products[index].mobileOffer.has_value() &&
        products[index].mobileOffer->ios.has_value())
    {
        return products[index].mobileOffer->ios->signature;
    }

    return {};
}

bool MegaPricingPrivate::hasMobileOfferAndroid(int productIndex) const
{
    if (auto index = static_cast<size_t>(productIndex);
        index < products.size() && products[index].mobileOffer.has_value())
    {
        return products[index].mobileOffer->android.has_value();
    }

    return false;
}

std::string MegaPricingPrivate::getMobileOfferAndroidOfferId(int productIndex) const
{
    if (auto index = static_cast<size_t>(productIndex);
        index < products.size() && products[index].mobileOffer.has_value() &&
        products[index].mobileOffer->android.has_value())
    {
        return products[index].mobileOffer->android->offerId;
    }

    return {};
}

const char *MegaPricingPrivate::getDescription(int productIndex)
{
    if (productIndex >= 0 && static_cast<unsigned int>(productIndex) < products.size())
    {
        return products[static_cast<size_t>(productIndex)].description.c_str();
    }

    return nullptr;
}

const char *MegaPricingPrivate::getIosID(int productIndex)
{
    if (productIndex >= 0 && static_cast<unsigned int>(productIndex) < products.size())
    {
        return products[static_cast<size_t>(productIndex)].iosid.c_str();
    }

    return nullptr;
}

const char *MegaPricingPrivate::getAndroidID(int productIndex)
{
    if (productIndex >= 0 && static_cast<unsigned int>(productIndex) < products.size())
    {
        return products[static_cast<size_t>(productIndex)].androidid.c_str();
    }

    return nullptr;
}

bool MegaPricingPrivate::isBusinessType(int productIndex)
{
    return isType(productIndex, PlanType::BUSINESS);
}

bool MegaPricingPrivate::isFeaturePlan(int productIndex) const
{
    return isType(productIndex, PlanType::FEATURE);
}

bool MegaPricingPrivate::isType(int productIndex, unsigned t) const
{
    if (productIndex >= 0 && static_cast<unsigned int>(productIndex) < products.size())
    {
        return products[static_cast<size_t>(productIndex)].planType == t;
    }

    return false;
}

int MegaPricingPrivate::getAmountMonth(int productIndex)
{
    if (productIndex >= 0 && static_cast<unsigned int>(productIndex) < products.size())
    {
        const auto v = products[static_cast<size_t>(productIndex)].amountMonth;
        return static_cast<int>(std::round(v));
    }

    return 0;
}

MegaPricing *MegaPricingPrivate::copy()
{
    MegaPricingPrivate *megaPricing = new MegaPricingPrivate();
    megaPricing->products = this->products;

    return megaPricing;
}

void MegaPricingPrivate::addProduct(const Product& product)
{
    this->products.push_back(product);
}

int MegaPricingPrivate::getGBStoragePerUser(int productIndex)
{
    // some Pro plans don't have a valid pointer, only business plans
    if (productIndex >= 0 && static_cast<unsigned int>(productIndex) < products.size() &&
        products[static_cast<size_t>(productIndex)].businessPlan)
    {
        return products[static_cast<size_t>(productIndex)].businessPlan->gbStoragePerUser;
    }

    return 0;
}

int MegaPricingPrivate::getGBTransferPerUser(int productIndex)
{
    // some Pro plans don't have a valid pointer, only business plans
    if (productIndex >= 0 && static_cast<unsigned int>(productIndex) < products.size() &&
        products[static_cast<size_t>(productIndex)].businessPlan)
    {
        return products[static_cast<size_t>(productIndex)].businessPlan->gbTransferPerUser;
    }

    return 0;
}

unsigned int MegaPricingPrivate::getMinUsers(int productIndex)
{
    // some Pro plans don't have a valid pointer, only business plans
    if (productIndex >= 0 && static_cast<unsigned int>(productIndex) < products.size() &&
        products[static_cast<size_t>(productIndex)].businessPlan)
    {
        return products[static_cast<size_t>(productIndex)].businessPlan->minUsers;
    }

    return 0;
}

unsigned int MegaPricingPrivate::getPricePerUser(int productIndex)
{
    // some Pro plans don't have a valid pointer, only business plans
    if (productIndex >= 0 && static_cast<unsigned int>(productIndex) < products.size() &&
        products[static_cast<size_t>(productIndex)].businessPlan)
    {
        return products[static_cast<size_t>(productIndex)].businessPlan->pricePerUser;
    }

    return 0;
}

unsigned int MegaPricingPrivate::getLocalPricePerUser(int productIndex)
{
    // some Pro plans don't have a valid pointer, only business plans
    if (productIndex >= 0 && static_cast<unsigned int>(productIndex) < products.size() &&
        products[static_cast<size_t>(productIndex)].businessPlan)
    {
        return products[static_cast<size_t>(productIndex)].businessPlan->localPricePerUser;
    }

    return 0;
}

unsigned int MegaPricingPrivate::getPricePerStorage(int productIndex)
{
    // some Pro plans don't have a valid pointer, only business plans
    if (productIndex >= 0 && static_cast<unsigned int>(productIndex) < products.size() &&
        products[static_cast<size_t>(productIndex)].businessPlan)
    {
        return products[static_cast<size_t>(productIndex)].businessPlan->pricePerStorage;
    }

    return 0;
}

unsigned int MegaPricingPrivate::getLocalPricePerStorage(int productIndex)
{
    // some Pro plans don't have a valid pointer, only business plans
    if (productIndex >= 0 && static_cast<unsigned int>(productIndex) < products.size() &&
        products[static_cast<size_t>(productIndex)].businessPlan)
    {
        return products[static_cast<size_t>(productIndex)].businessPlan->localPricePerStorage;
    }

    return 0;
}

int MegaPricingPrivate::getGBPerStorage(int productIndex)
{
    // some Pro plans don't have a valid pointer, only business plans
    if (productIndex >= 0 && static_cast<unsigned int>(productIndex) < products.size() &&
        products[static_cast<size_t>(productIndex)].businessPlan)
    {
        return products[static_cast<size_t>(productIndex)].businessPlan->gbPerStorage;
    }

    return 0;
}

unsigned int MegaPricingPrivate::getPricePerTransfer(int productIndex)
{
    // some Pro plans don't have a valid pointer, only business plans
    if (productIndex >= 0 && static_cast<unsigned int>(productIndex) < products.size() &&
        products[static_cast<size_t>(productIndex)].businessPlan)
    {
        return products[static_cast<size_t>(productIndex)].businessPlan->pricePerTransfer;
    }

    return 0;
}

unsigned int MegaPricingPrivate::getLocalPricePerTransfer(int productIndex)
{
    // some Pro plans don't have a valid pointer, only business plans
    if (productIndex >= 0 && static_cast<unsigned int>(productIndex) < products.size() &&
        products[static_cast<size_t>(productIndex)].businessPlan)
    {
        return products[static_cast<size_t>(productIndex)].businessPlan->localPricePerTransfer;
    }

    return 0;
}

int MegaPricingPrivate::getGBPerTransfer(int productIndex)
{
    // some Pro plans don't have a valid pointer, only business plans
    if (productIndex >= 0 && static_cast<unsigned int>(productIndex) < products.size() &&
        products[static_cast<size_t>(productIndex)].businessPlan)
    {
        return products[static_cast<size_t>(productIndex)].businessPlan->gbPerTransfer;
    }

    return 0;
}

MegaStringIntegerMap* MegaPricingPrivate::getFeatures(int productIndex) const
{
    if ((unsigned)productIndex >= products.size())
    {
        return nullptr;
    }

    MegaStringIntegerMapPrivate* returnFeatures = new MegaStringIntegerMapPrivate();
    for (const auto& feature: products[static_cast<size_t>(productIndex)].features)
    {
        returnFeatures->set(feature.first, feature.second);
    }
    return returnFeatures;
}

unsigned int MegaPricingPrivate::getTestCategory(int productIndex) const
{
    if ((unsigned)productIndex < products.size())
    {
        return products[static_cast<size_t>(productIndex)].testCategory;
    }

    return 0;
}

unsigned int MegaPricingPrivate::getTrialDurationInDays(int productIndex) const
{
    if ((unsigned)productIndex < products.size())
    {
        return products[static_cast<size_t>(productIndex)].trialDays;
    }

    return 0;
}

const InstantDiscounts* MegaPricingPrivate::getInstantDiscounts(const int productIndex) const
{
    if (productIndex < 0 || static_cast<size_t>(productIndex) >= products.size())
    {
        return nullptr;
    }

    if (const auto& opt = products[static_cast<size_t>(productIndex)].instantDiscounts;
        opt.has_value())
    {
        return &opt.value();
    }

    return nullptr;
}

bool MegaPricingPrivate::hasDiscount(int productIndex) const
{
    return getInstantDiscounts(productIndex);
}

const char* MegaPricingPrivate::getDiscountCode(int productIndex) const
{
    if (const auto* discounts = getInstantDiscounts(productIndex))
    {
        return discounts->discountCode.c_str();
    }

    return nullptr;
}

const char* MegaPricingPrivate::getDiscountName(int productIndex) const
{
    if (const auto* discounts = getInstantDiscounts(productIndex))
    {
        return discounts->discountName.c_str();
    }

    return nullptr;
}

int MegaPricingPrivate::getDiscountGroup(int productIndex) const
{
    if (const auto* discounts = getInstantDiscounts(productIndex))
    {
        return discounts->discountGroup;
    }

    return -1;
}

int MegaPricingPrivate::getDiscountMonths(int productIndex) const
{
    if (const auto* discounts = getInstantDiscounts(productIndex))
    {
        return discounts->discountMonths;
    }

    return -1;
}

int MegaPricingPrivate::getDiscountPercentage(int productIndex) const
{
    if (const auto* discounts = getInstantDiscounts(productIndex))
    {
        return discounts->discountPercentage;
    }

    return -1;
}

MegaCurrencyPrivate::~MegaCurrencyPrivate()
{
}

MegaCurrency *MegaCurrencyPrivate::copy()
{
    return new MegaCurrencyPrivate(*this);
}

const char *MegaCurrencyPrivate::getCurrencySymbol()
{
    return mCurrencyData.currencySymbol.c_str();
}

const char *MegaCurrencyPrivate::getCurrencyName()
{
    return mCurrencyData.currencyName.c_str();
}

const char *MegaCurrencyPrivate::getLocalCurrencySymbol()
{
    return mCurrencyData.localCurrencySymbol.c_str();
}

const char *MegaCurrencyPrivate::getLocalCurrencyName()
{
    return mCurrencyData.localCurrencyName.c_str();
}

void MegaCurrencyPrivate::setCurrency(std::unique_ptr<CurrencyData> data)
{
    assert(data);
    mCurrencyData = *data;
}

#ifdef ENABLE_SYNC
MegaSyncPrivate::MegaSyncPrivate(const SyncConfig& config, MegaClient* client /* never null */)
    : mRunState(MegaSync::SyncRunningState(config.mRunState))
    , mType(static_cast<SyncType>(config.getType()))
{
    this->megaHandle = config.mRemoteNode.as8byte();
    this->localFolder = NULL;
    setLocalFolder(config.getLocalPath().toPath(false).c_str());
    this->mName= NULL;
    if (!config.mName.empty())
    {
        setName(config.mName.c_str());
    }
    else
    {
        //using leaf name of localpath as name:
        setName(config.getLocalPath().leafName().toName(*client->fsaccess).c_str());
    }
    this->lastKnownMegaFolder = NULL;

    setLastKnownMegaFolder(config.mOriginalPathOfRemoteRootNode.c_str());

    setError(config.mError < 0 ? 0 : config.mError);
    setWarning(config.mWarning);
    setBackupId(config.mBackupId);
}

MegaSyncPrivate::MegaSyncPrivate(MegaSyncPrivate *sync)
    : mRunState(sync->mRunState)
    , mType(sync->mType)
{
    this->setBackupId(sync->mBackupId);
    this->localFolder = NULL;
    this->mName = NULL;
    this->setLocalFolder(sync->getLocalFolder());
    this->setName(sync->getName());
    this->lastKnownMegaFolder = NULL;
    this->setLastKnownMegaFolder(sync->getLastKnownMegaFolder());
    this->setMegaHandle(sync->getMegaHandle());
    this->setError(sync->getError());
    this->setWarning(sync->getWarning());
}

MegaSyncPrivate::~MegaSyncPrivate()
{
    delete [] localFolder;
    delete [] mName;
    delete [] lastKnownMegaFolder;
}

MegaSync *MegaSyncPrivate::copy()
{
    return new MegaSyncPrivate(this);
}

MegaHandle MegaSyncPrivate::getMegaHandle() const
{
    return megaHandle;
}

void MegaSyncPrivate::setMegaHandle(MegaHandle handle)
{
    this->megaHandle = handle;
}

const char *MegaSyncPrivate::getLocalFolder() const
{
    return localFolder;
}

void MegaSyncPrivate::setLocalFolder(const char *path)
{
    if (localFolder)
    {
        delete [] localFolder;
    }
    localFolder =  MegaApi::strdup(path);
}

const char *MegaSyncPrivate::getName() const
{
    return mName;
}

void MegaSyncPrivate::setName(const char *name)
{
    if (mName)
    {
        delete [] mName;
    }
    mName =  MegaApi::strdup(name);
}

const char *MegaSyncPrivate::getLastKnownMegaFolder() const
{
    return lastKnownMegaFolder;
}

void MegaSyncPrivate::setLastKnownMegaFolder(const char *path)
{
    if (lastKnownMegaFolder)
    {
        delete [] lastKnownMegaFolder;
    }
    lastKnownMegaFolder = MegaApi::strdup(path);
}

MegaHandle MegaSyncPrivate::getBackupId() const
{
    return mBackupId;
}

void MegaSyncPrivate::setBackupId(MegaHandle backupId)
{
    this->mBackupId = backupId;
}

int MegaSyncPrivate::getError() const
{
    return mError;
}

void MegaSyncPrivate::setError(int error)
{
    mError = error;
}

int MegaSyncPrivate::getWarning() const
{
    return mWarning;
}

void MegaSyncPrivate::setWarning(int warning)
{
    mWarning = warning;
}

int MegaSyncPrivate::getType() const
{
    return mType;
}

void MegaSyncPrivate::setType(MegaSync::SyncType type)
{
    mType = type;
}

int MegaSyncPrivate::getRunState() const
{
    return mRunState;
}

MegaSyncListPrivate::MegaSyncListPrivate()
{
    list = NULL;
    s = 0;
}

MegaSyncListPrivate::MegaSyncListPrivate(MegaSyncPrivate** newlist, int size)
{
    list = NULL; s = size;
    if(!size) return;

    list = new MegaSync*[static_cast<size_t>(size)];
    for(int i=0; i<size; i++)
        list[i] = newlist[i]->copy();
}

MegaSyncListPrivate::MegaSyncListPrivate(const MegaSyncListPrivate *syncList)
{
    s = syncList->size();
    if (!s)
    {
        list = NULL;
        return;
    }

    list = new MegaSync*[static_cast<size_t>(s)];
    for (int i = 0; i<s; i++)
    {
        list[i] = new MegaSyncPrivate(static_cast<MegaSyncPrivate *>(syncList->get(i)));
    }
}

MegaSyncListPrivate::~MegaSyncListPrivate()
{
    if(!list)
        return;

    for(int i=0; i<s; i++)
        delete list[i];
    delete [] list;
}

MegaSyncList *MegaSyncListPrivate::copy() const
{
    return new MegaSyncListPrivate(this);
}

MegaSync *MegaSyncListPrivate::get(int i) const
{
    if(!list || (i < 0) || (i >= s))
        return NULL;

    return list[i];
}

int MegaSyncListPrivate::size() const
{
    return s;
}

void MegaSyncListPrivate::addSync(MegaSync *sync)
{
    MegaSync** copyList = list;
    s = s + 1;
    list = new MegaSync*[static_cast<size_t>(s)];
    for (int i = 0; i < s - 1; ++i)
    {
        list[i] = copyList[i];
    }

    list[s - 1] = sync->copy();

    if (copyList != NULL)
    {
        delete [] copyList;
    }
}
#endif


MegaAccountBalance *MegaAccountBalancePrivate::fromAccountBalance(const AccountBalance *balance)
{
    return new MegaAccountBalancePrivate(balance);
}

MegaAccountBalancePrivate::~MegaAccountBalancePrivate()
{

}

MegaAccountBalance *MegaAccountBalancePrivate::copy()
{
    return new MegaAccountBalancePrivate(&balance);
}

double MegaAccountBalancePrivate::getAmount() const
{
    return balance.amount;
}

char *MegaAccountBalancePrivate::getCurrency() const
{
    return MegaApi::strdup(balance.currency);
}

MegaAccountBalancePrivate::MegaAccountBalancePrivate(const AccountBalance *balance)
{
    this->balance = *balance;
}

MegaAccountSession *MegaAccountSessionPrivate::fromAccountSession(const AccountSession *session)
{
    return new MegaAccountSessionPrivate(session);
}

MegaAccountSessionPrivate::~MegaAccountSessionPrivate()
{

}

MegaAccountSession *MegaAccountSessionPrivate::copy()
{
    return new MegaAccountSessionPrivate(&session);
}

int64_t MegaAccountSessionPrivate::getCreationTimestamp() const
{
    return session.timestamp;
}

int64_t MegaAccountSessionPrivate::getMostRecentUsage() const
{
    return session.mru;
}

char *MegaAccountSessionPrivate::getUserAgent() const
{
    return MegaApi::strdup(session.useragent.c_str());
}

char *MegaAccountSessionPrivate::getIP() const
{
    return MegaApi::strdup(session.ip.c_str());
}

char *MegaAccountSessionPrivate::getCountry() const
{
    return MegaApi::strdup(session.country);
}

bool MegaAccountSessionPrivate::isCurrent() const
{
    return session.current != 0;
}

bool MegaAccountSessionPrivate::isAlive() const
{
    return session.alive != 0;
}

MegaHandle MegaAccountSessionPrivate::getHandle() const
{
    return session.id;
}

char *MegaAccountSessionPrivate::getDeviceId() const
{
    return MegaApi::strdup(session.deviceid.c_str());
}

MegaAccountSessionPrivate::MegaAccountSessionPrivate(const AccountSession *session)
{
    this->session = *session;
}


MegaAccountPurchase *MegaAccountPurchasePrivate::fromAccountPurchase(const AccountPurchase *purchase)
{
    return new MegaAccountPurchasePrivate(purchase);
}

MegaAccountPurchasePrivate::~MegaAccountPurchasePrivate()
{

}

MegaAccountPurchase *MegaAccountPurchasePrivate::copy()
{
    return new MegaAccountPurchasePrivate(&purchase);
}

int64_t MegaAccountPurchasePrivate::getTimestamp() const
{
    return purchase.timestamp;
}

char *MegaAccountPurchasePrivate::getHandle() const
{
    return MegaApi::strdup(purchase.handle);
}

char *MegaAccountPurchasePrivate::getCurrency() const
{
    return MegaApi::strdup(purchase.currency);
}

double MegaAccountPurchasePrivate::getAmount() const
{
    return purchase.amount;
}

int MegaAccountPurchasePrivate::getMethod() const
{
    return purchase.method;
}

MegaAccountPurchasePrivate::MegaAccountPurchasePrivate(const AccountPurchase *purchase)
{
    this->purchase = *purchase;
}


MegaAccountTransaction *MegaAccountTransactionPrivate::fromAccountTransaction(const AccountTransaction *transaction)
{
    return new MegaAccountTransactionPrivate(transaction);
}

MegaAccountTransactionPrivate::~MegaAccountTransactionPrivate()
{

}

MegaAccountTransaction *MegaAccountTransactionPrivate::copy()
{
    return new MegaAccountTransactionPrivate(&transaction);
}

int64_t MegaAccountTransactionPrivate::getTimestamp() const
{
    return transaction.timestamp;
}

char *MegaAccountTransactionPrivate::getHandle() const
{
    return MegaApi::strdup(transaction.handle);
}

char *MegaAccountTransactionPrivate::getCurrency() const
{
    return MegaApi::strdup(transaction.currency);
}

double MegaAccountTransactionPrivate::getAmount() const
{
    return transaction.delta;
}

MegaAccountTransactionPrivate::MegaAccountTransactionPrivate(const AccountTransaction *transaction)
{
    this->transaction = *transaction;
}


MegaAccountFeaturePrivate* MegaAccountFeaturePrivate::fromAccountFeature(const AccountFeature* feature)
{
    return new MegaAccountFeaturePrivate(feature);
}

MegaAccountFeaturePrivate::MegaAccountFeaturePrivate(const AccountFeature* feature)
{
    assert(feature);
    if (feature)
    {
        mFeature = *feature;
    }
}

int64_t MegaAccountFeaturePrivate::getExpiry() const
{
    return mFeature.expiryTimestamp;
}

char* MegaAccountFeaturePrivate::getId() const
{
    return MegaApi::strdup(mFeature.featureId.c_str());
}

MegaAccountSubscriptionPrivate*
    MegaAccountSubscriptionPrivate::fromAccountSubscription(const AccountSubscription& subscription)
{
    return new MegaAccountSubscriptionPrivate(subscription);
}

MegaAccountSubscriptionPrivate::MegaAccountSubscriptionPrivate(
    const AccountSubscription& subscription)
{
    mSubscription = subscription;
}

char* MegaAccountSubscriptionPrivate::getId() const
{
    return MegaApi::strdup(mSubscription.id.c_str());
}

int MegaAccountSubscriptionPrivate::getStatus() const
{
    if (mSubscription.type == 'S')
    {
        return MegaAccountSubscription::SUBSCRIPTION_STATUS_VALID;
    }

    if (mSubscription.type == 'R')
    {
        return MegaAccountSubscription::SUBSCRIPTION_STATUS_INVALID;
    }

    return MegaAccountSubscription::SUBSCRIPTION_STATUS_NONE;
}

char* MegaAccountSubscriptionPrivate::getCycle() const
{
    return MegaApi::strdup(mSubscription.cycle.c_str());
}

char* MegaAccountSubscriptionPrivate::getPaymentMethod() const
{
    return MegaApi::strdup(mSubscription.paymentMethod.c_str());
}

int32_t MegaAccountSubscriptionPrivate::getPaymentMethodId() const
{
    return mSubscription.paymentMethodId;
}

int64_t MegaAccountSubscriptionPrivate::getRenewTime() const
{
    return mSubscription.renew;
}

int32_t MegaAccountSubscriptionPrivate::getAccountLevel() const
{
    return mSubscription.level;
}

MegaStringList* MegaAccountSubscriptionPrivate::getFeatures() const
{
    MegaStringListPrivate* subscriptionFeatures = new MegaStringListPrivate();
    for (const auto& f: mSubscription.features)
    {
        subscriptionFeatures->add(f.c_str());
    }
    return subscriptionFeatures;
}

bool MegaAccountSubscriptionPrivate::isTrial() const
{
    return mSubscription.isTrial;
}

MegaAccountPlanPrivate* MegaAccountPlanPrivate::fromAccountPlan(const AccountPlan& plan)
{
    return new MegaAccountPlanPrivate(plan);
}

MegaAccountPlanPrivate::MegaAccountPlanPrivate(const AccountPlan& plan)
{
    mPlan = plan;
}

bool MegaAccountPlanPrivate::isProPlan() const
{
    return (mPlan.level > MegaAccountDetails::ACCOUNT_TYPE_FREE &&
            mPlan.level != MegaAccountDetails::ACCOUNT_TYPE_FEATURE);
}

int32_t MegaAccountPlanPrivate::getAccountLevel() const
{
    return mPlan.level;
}

MegaStringList* MegaAccountPlanPrivate::getFeatures() const
{
    MegaStringListPrivate* planFeatures = new MegaStringListPrivate();
    for (const auto& f: mPlan.features)
    {
        planFeatures->add(f.c_str());
    }
    return planFeatures;
}

int64_t MegaAccountPlanPrivate::getExpirationTime() const
{
    return mPlan.expiration;
}

int64_t MegaAccountPlanPrivate::getStartTime() const
{
    return mPlan.startTime;
}

int32_t MegaAccountPlanPrivate::getType() const
{
    return mPlan.type;
}

char* MegaAccountPlanPrivate::getId() const
{
    return MegaApi::strdup(mPlan.subscriptionId.c_str());
}

bool MegaAccountPlanPrivate::isTrial() const
{
    return mPlan.isTrial;
}

ExternalInputStream::ExternalInputStream(MegaInputStream *inputStream)
{
    this->inputStream = inputStream;
}

m_off_t ExternalInputStream::size()
{
    return inputStream->getSize();
}

bool ExternalInputStream::read(byte *buffer, unsigned size)
{
    return inputStream->read((char *)buffer, size);
}


MegaTreeProcCopy::MegaTreeProcCopy(MegaClient *client)
{
    this->client = client;
}

void MegaTreeProcCopy::allocnodes()
{
    nn.resize(nc);
    allocated = true;
}
bool MegaTreeProcCopy::processMegaNode(MegaNode *n)
{
    // MegaNode has no keyApplied(), so check the key length directly. A file node whose
    // key was never applied cannot be copied.
    if (n->getType() == MegaNode::TYPE_FILE &&
        (!n->getNodeKey() || n->getNodeKey()->size() != FILENODEKEYLENGTH))
    {
        unusableKey = true;
        LOG_err << "MegaTreeProcCopy: node " << toNodeHandle(n->getHandle()) << " ("
                << (n->getName() ? n->getName() : "?")
                << ") has an unapplied key, import will be aborted";
        return true;
    }

    if (allocated)
    {
        // prepare map of attributes
        AttrMap attrs;
        {
            string sname = n->getName();
            LocalPath::utf8_normalize(&sname);
            attrs.map['n'] = sname;
        }
        {
            string sfp = MegaNodePrivate::removeAppPrefixFromFingerprint(n->getFingerprint());
            if (!sfp.empty())
            {
                attrs.map['c'] = std::move(sfp);
            }
        }

        client->putnodes_prepareCopy(nn,
                                     nc,
                                     static_cast<nodetype_t>(n->getType()),
                                     n->getHandle(),
                                     n->getParentHandle() ? n->getParentHandle() : UNDEF,
                                     *n->getNodeKey(),
                                     attrs,
                                     false,
                                     n->isPublic());
    }
    else
    {
        nc++;
    }

    return true;
}

MegaFolderUploadController::MegaFolderUploadController(MegaApiImpl* api, MegaTransferPrivate* t):
    MegaRecursiveOperation(api->getMegaClient())
{
    fsaccess = mega::createFSA();
    megaApi = api;
    transfer = t;
    listener = t->getListener();
    recursive = 0;
    tag = transfer->getTag();
    mMainThreadId = std::this_thread::get_id();

    assert(mMainThreadId == megaApi->threadId);
}

void MegaFolderUploadController::start(MegaNode*)
{
    assert(mMainThreadId == std::this_thread::get_id());

    transfer->setFolderTransferTag(-1);
    transfer->setStartTime(Waiter::ds);
    transfer->setState(MegaTransfer::STATE_QUEUED);
    megaApi->fireOnTransferStart(transfer);

    unique_ptr<MegaNode> parent(megaApi->getNodeByHandle(transfer->getParentHandle()));
    if (!parent)
    {
        transfer->setState(MegaTransfer::STATE_FAILED);
        megaApi->fireOnTransferFinish(transfer, std::make_unique<MegaErrorPrivate>(API_EARGS));
        return;
    }

    // set transfer target node as megaNode of root tree (mUploadTree)
    mUploadTree.megaNode.reset(parent.release());

    // create a subtree for the folder that we want to upload
    unique_ptr<Tree> newTreeNode(new Tree);
    LocalPath path = LocalPath::fromAbsolutePath(transfer->getPath());
    auto leaf = transfer->getFileName()
            ? transfer->getFileName()
            : path.leafName().toPath(true);

    // if folder node already exists in remote, set it as new subtree's megaNode, otherwise call putnodes_prepareOneFolder
    newTreeNode->megaNode.reset(megaApi->getChildNode(mUploadTree.megaNode.get(), leaf.c_str()));

    if (newTreeNode->megaNode && newTreeNode->megaNode->getType() == MegaNode::TYPE_FILE)
    {
        // there's a node (TYPE_FILE) with the same name in the destination path, we will make fail transfer
        transfer->setState(MegaTransfer::STATE_FAILED);
        megaApi->fireOnTransferFinish(transfer, std::make_unique<MegaErrorPrivate>(API_EEXIST));
        return;
    }
    else if (!newTreeNode->megaNode) // there's no other node with the same name in the destination path
    {
        newTreeNode->folderName = leaf;
        newTreeNode->fsType = fsaccess->getlocalfstype(path);

        megaapiThreadClient()->putnodes_prepareOneFolder(&newTreeNode->newnode, leaf, false);
        newTreeNode->newnode.nodehandle = nextUploadId();
        newTreeNode->newnode.parenthandle = UNDEF;
        mUploadIdToTree[newTreeNode->newnode.nodehandle] = newTreeNode.get();
    }
    // else => if there's another node (TYPE_FOLDER) with the same name, in the destination path, the content of both folders will be merged

    // add the tree above, to subtrees vector for root tree
    mUploadTree.subtrees.push_back(std::move(newTreeNode));

    // it's mandatory to notify stage change from MegaApiImpl's thread to avoid deadlocks and other issues
    notifyStage(MegaTransfer::STAGE_SCAN);

    mWorkerThread = std::thread ([this, path]() {
        // recurse all subfolders on disk, building up tree structure to match
        // not yet existing folders get a temporary upload id instead of a handle
        uint32_t foldercount = 0;
        uint32_t filecount = 0;
        LocalPath lp = path;
        scanFolder_result scanResult = scanFolder(*mUploadTree.subtrees.front(), lp, foldercount, filecount);

        // mCompletionForMegaApiThread lambda will be executed on the MegaApiImpl's thread
        // use a weak_ptr in case this 'this' object doesn't exist anymore when lambda starts executing
        weak_ptr<MegaFolderUploadController> weak_this = shared_from_this();

        // if the thread runs, we always queue a function to execute on MegaApi thread for onFinish()
        // we keep a pointer to it in case we need to execute it early and directly on cancel()
        mCompletionForMegaApiThread.reset(new ExecuteOnce([this, scanResult, weak_this, filecount]() {

            // double check our object still exists when completion function starts executing
            if (!weak_this.lock()) return;
            assert(weak_this.lock().get() == this);

            // these next parts must run on MegaApiImpl's thread again, as
            // genUploadTransfersForFiles or checkCompletion may call the fireOnXYZ() functions
            assert(mMainThreadId == std::this_thread::get_id());

            // make sure the thread is joined.  This lets us add error-catching asserts elsewhere.
            if (mWorkerThread.joinable())
            {
                mWorkerThread.join();
            }

            if (scanResult == scanFolder_failed)
            {
                // scan stage could not finish properly, because some dir could not be accessed
                complete(API_EACCESS);
                return;
            }
            else if (scanResult == scanFolder_cancelled)
            {
                complete(API_EINCOMPLETE, true);
                return;
            }

            // create folders in batches, not too many at once
            // createNextFolderBatch is responsible for starting the transfers once all needed folders (if any) are created.
            notifyStage(MegaTransfer::STAGE_CREATE_TREE);
            vector<NewNode> newnodes;
#ifndef NDEBUG
            batchResult r =
#endif
            createNextFolderBatch(mUploadTree, newnodes, filecount, true);

            assert(r == batchResult_cancelled || r == batchResult_requestSent ||
                   r == batchResult_batchesComplete || r == batchResult_failed);
        }));

        // Queue that function.
        megaApi->executeOnThread(mCompletionForMegaApiThread);
    });
}

// this method provides a temporal handle useful to indicate putnodes()-local parent linkage
handle MegaFolderUploadController::nextUploadId()
{
    return mCurrUploadId = (mCurrUploadId + 1 <= 0xFFFFFFFFFFFF)
        ? mCurrUploadId + 1
        : 0;
}

void MegaRecursiveOperation::onTransferStart(MegaApi *, MegaTransfer *t)
{
    assert(mMainThreadId == std::this_thread::get_id());
    assert(transfer);

    ++transfersStartedCount;
    if (transfersStartedCount == transfersTotalCount &&
        !transfer->accessCancelToken().isCancelled() &&
        !startedTransferring)
    {
        // Apps expect this one called when all sub-transfers have
        // been queued in SDK core already
        notifyStage(MegaTransfer::STAGE_TRANSFERRING_FILES);
        megaApi->fireOnFolderTransferUpdate(transfer, MegaTransfer::STAGE_TRANSFERRING_FILES, 0, 0, unsigned(transfersTotalCount), nullptr, nullptr);
        startedTransferring = true;
    }

    if (transfer)
    {
        transfer->setState(t->getState());
        transfer->setPriority(t->getPriority());
        transfer->setTotalBytes(transfer->getTotalBytes() + t->getTotalBytes());
        transfer->setUpdateTime(Waiter::ds);
        megaApi->fireOnTransferUpdate(transfer);
    }
}

void MegaRecursiveOperation::onTransferUpdate(MegaApi *, MegaTransfer *t)
{
    assert(mMainThreadId == std::this_thread::get_id());
    assert(transfer);
    if (transfer)
    {
        LOG_verbose << "MegaRecursiveOperation: on transfer update -> adding new progress " << t->getDeltaSize() << " to previous transferred bytes " << transfer->getTransferredBytes() << " -> updated transferred bytes = " << (transfer->getTransferredBytes() + t->getDeltaSize());
        transfer->setState(t->getState());
        transfer->setPriority(t->getPriority());
        transfer->setTransferredBytes(transfer->getTransferredBytes() + t->getDeltaSize());
        transfer->setUpdateTime(Waiter::ds);
        transfer->setSpeed(t->getSpeed());
        transfer->setMeanSpeed(t->getMeanSpeed());
        megaApi->fireOnTransferUpdate(transfer);
    }
}

void MegaRecursiveOperation::onTransferFinish(MegaApi *, MegaTransfer *t, MegaError *e)
{
    assert(mMainThreadId == std::this_thread::get_id());
    ++transfersFinishedCount;
    assert(transfer);
    if (transfer)
    {
        LOG_verbose << "MegaRecursiveOperation: on transfer finish -> adding new progress " << t->getDeltaSize() << " to previous transferred bytes " << transfer->getTransferredBytes() << " -> updated transferred bytes = " << (transfer->getTransferredBytes() + t->getDeltaSize());
        transfer->setState(MegaTransfer::STATE_ACTIVE);
        transfer->setPriority(t->getPriority());
        transfer->setTransferredBytes(transfer->getTransferredBytes() + t->getDeltaSize());
        transfer->setUpdateTime(Waiter::ds);
        transfer->setSpeed(t->getSpeed());
        transfer->setMeanSpeed(t->getMeanSpeed());
        megaApi->fireOnTransferUpdate(transfer);
    }
    if (e->getErrorCode() != API_OK)
    {
        mIncompleteTransfers++;
    }
    LOG_debug << "MegaRecursiveOperation finished subtransfers: " << transfersFinishedCount << " of " << transfersTotalCount;
    if (allSubtransfersResolved())
    {
        setRootNodeHandleInTransfer();

        // Cancelled or not, there is always an onTransferFinish callback for the folder transfer.
        // If subtransfers were started, completion is always by the last subtransfer completing
        complete(mIncompleteTransfers ? API_EINCOMPLETE : API_OK);
    }
}

MegaFolderUploadController::~MegaFolderUploadController()
{
    assert(mMainThreadId == std::this_thread::get_id());
    LOG_debug << "MegaFolderUploadController dtor is being called from main thread";
    ensureThreadStopped();

    //we shouldn't need to detach as transfer listener: all listened transfer should have been cancelled/completed
}

MegaFolderUploadController::scanFolder_result MegaFolderUploadController::scanFolder(Tree& tree, LocalPath& localPath, uint32_t& foldercount, uint32_t& filecount)
{
    recursive++;
    unique_ptr<DirAccess> da(fsaccess->newdiraccess());
    if (!da->dopen(&localPath, nullptr, false))
    {
        LOG_err << "Can't open local directory" << localPath;
        recursive--;
        return scanFolder_failed;
    }

    megaApi->fireOnFolderTransferUpdate(transfer, MegaTransfer::STAGE_SCAN, foldercount, 0, filecount, &localPath, nullptr);

    LocalPath localname;
    nodetype_t dirEntryType;
    LocalPath childPath = localPath;
    while (da->dnext(childPath, localname, false, &dirEntryType))
    {
        if (isStoppedOrCancelled("MegaFolderUploadController::scanFolder"))
        {
            return scanFolder_cancelled;
        }

        megaApi->fireOnFolderTransferUpdate(transfer, MegaTransfer::STAGE_SCAN, foldercount, 0, filecount, &localPath, &localname);

        if (!childPath.isURI())
        {
            childPath.appendWithSeparator(localname, false);
        }

        if (dirEntryType == FILENODE)
        {
            // Do the fingerprinting for uploads on the scan thread, so we don't lock the main mutex for so long
            FileFingerprint fp;
            auto fa = fsaccess->newfileaccess();
            if (fa->fopen(childPath, OPEN_RDONLY, FSLogging::logOnError))
            {
                fp.genfingerprint(fa.get());
            }

            // if we couldn't get the fingerprint, !isvalid and we'll fail the transfer
            tree.files.emplace_back(childPath, fp);

            filecount += 1;
        }
        else if (dirEntryType == FOLDERNODE)
        {
            // generate new subtree
            unique_ptr<Tree> newTreeNode(new Tree);
            newTreeNode->folderName = localname.toName(*fsaccess);
            newTreeNode->fsType = fsaccess->getlocalfstype(childPath);

            // generate fresh random key and node attributes
            MegaClient::putnodes_prepareOneFolder(&newTreeNode->newnode, newTreeNode->folderName, rng, tmpnodecipher, false);

            // set nodeHandle
            newTreeNode->newnode.nodehandle = nextUploadId();
            newTreeNode->newnode.parenthandle = tree.newnode.nodehandle;
            mUploadIdToTree[newTreeNode->newnode.nodehandle] = newTreeNode.get();

            scanFolder_result sr = scanFolder(*newTreeNode, childPath, foldercount, filecount);
            if (sr != scanFolder_succeeded)
            {
                recursive--;
                return sr;
            }
            tree.subtrees.push_back(std::move(newTreeNode));

            foldercount += 1;
        }

        childPath = localPath;
    }
    recursive--;
    return scanFolder_succeeded;
}

MegaFolderUploadController::batchResult MegaFolderUploadController::createNextFolderBatch(Tree& tree, vector<NewNode>& newnodes, uint32_t filecount, bool isBatchRootLevel)
{
    assert(mMainThreadId == std::this_thread::get_id());

    // preload children for this level (optimization to speed up searches by name/type)
    // (done here instead of at scanFolder() to avoid locking the mutex from the worker)
    if (!tree.childrenLoaded && tree.megaNode)
    {
        std::shared_ptr<Node> parent = megaApi->client->nodebyhandle(tree.megaNode->getHandle());
        assert(parent);
        megaApi->client->getChildren(parent.get());
        tree.childrenLoaded = true;
    }

    bool hasMissingChildren = false;

    if (isBatchRootLevel)
    {
        for (auto& t: tree.subtrees)
        {
            if (!t->megaNode && tree.megaNode)
            {
                t->megaNode.reset(megaApi->getChildNodeOfType(tree.megaNode.get(),
                                                              t->folderName.c_str(),
                                                              MegaNode::TYPE_FOLDER));
            }

            if (!t->megaNode)
            {
                hasMissingChildren = true;
                break;
            }
        }
    }

    // recurse until we find nodes not yet created
    for (auto& t: tree.subtrees)
    {
        assert(newnodes.size() <= MAXNODESUPLOAD);
        if (newnodes.size() >= MAXNODESUPLOAD)
        {
           // avoid iterating through tree structure when a batch has reached the limit of nodes
           break;
        }

        if (!t->megaNode && tree.megaNode) // check if our last call created it (or it always existed)
        {
            t->megaNode.reset(megaApi->getChildNodeOfType(tree.megaNode.get(),
                                                          t->folderName.c_str(),
                                                          MegaNode::TYPE_FOLDER));
        }

        // test-only: let a test simulate this already-sent folder vanishing
        DEBUG_TEST_HOOK_FOLDER_UPLOAD_SIMULATE_MISSING(t->folderName, t->megaNode, t->newnodeSent);

        if (!t->megaNode && t->newnodeSent)
        {
            // We already created this folder in a previous batch, but it can no longer be
            // resolved (not by name, nor in the not-yet-committed set). It was most likely
            // removed by another session. Its newnode has already been moved out, so we must
            // not send it again (dereference a nullptr), fail the whole upload instead.
            LOG_err << "Folder upload: folder '" << t->folderName
                    << "' was created earlier but is missing now";
            complete(API_ENOENT);
            return batchResult_failed;
        }

        // if node doesn't exist yet and we haven't exceeded the limit per batch
        if (!t->megaNode && newnodes.size() < MAXNODESUPLOAD)
        {
            if (isBatchRootLevel)
            {
                /* the parent of the root newNode (for current batch) must already exist in remote,
                 * so parent handle for root newNode must be UNDEF */
                assert(tree.megaNode);
                t->newnode.parenthandle = UNDEF;
            }
            t->newnodeSent = true;
            newnodes.push_back(std::move(t->newnode));
        }

        if (isBatchRootLevel && hasMissingChildren && t->megaNode)
        {
            // If this level has missing children, only recurse into those branches.
            continue;
        }

        // if newnodes contains at least one newNode, isBatchRootLevel will be false
        batchResult br = createNextFolderBatch(*t, newnodes, filecount, newnodes.empty());
        if (br != batchResult_stillRecursing)
        {
            return br;
        }
    }

    if (isCancelledByFolderTransferToken())
    {
        complete(API_EINCOMPLETE, true);
        return batchResult_cancelled;
    }

    if (isBatchRootLevel && !newnodes.empty())
    {
        // the lambda will be exeuted on the MegaApiImpl's thread
        // use a weak_ptr in case this operation was cancelled, and 'this' object doesn't exist
        // anymore when the request completes
        weak_ptr<MegaFolderUploadController> weak_this = shared_from_this();

        auto parent = megaApi->client->mNodeManager.getNodeByHandle(
            NodeHandle().set6byte(tree.megaNode->getHandle()));
        const bool inIncomingShare = parent && parent->matchesOrHasAncestorMatching(
                                                   [](const Node& node)
                                                   {
                                                       return node.inshare != nullptr;
                                                   });
        Pitag localPitag = transfer->getPitag();
        localPitag.target = inIncomingShare ? PitagTarget::IncomingShare : PitagTarget::CloudDrive;

        megaapiThreadClient()->putnodes(
            NodeHandle().set6byte(tree.megaNode->getHandle()),
            UseLocalVersioningFlag,
            std::move(newnodes),
            nullptr,
            megaapiThreadClient()->nextreqtag(),
            false,
            {}, // customerIpPort
            [this, weak_this, filecount](const Error& e,
                                         targettype_t,
                                         vector<NewNode>& nn,
                                         bool,
                                         int /*tag*/,
                                         const map<string, string>& /*fileHandles*/)
            {
                // Hold a strong reference for the whole callback so the controller (and thus
                // mUploadTree / mUploadIdToTree accessed below) cannot be destroyed mid-callback.
                auto self = weak_this.lock();
                if (!self)
                    return;
                assert(self.get() == this);
                assert(mMainThreadId == std::this_thread::get_id());

                // command-level failure (also covers the all-nodes-failed case)
                if (e)
                {
                    complete(e);
                    return;
                }

                // test-only: let a test inject per-node putnodes errors (see testhooks.h)
                DEBUG_TEST_HOOK_FOLDER_UPLOAD_PUTNODES_RESULT(nn);

                // The overall result can be API_OK even when individual nodes failed, so
                // inspect the per-node errors. Any failure aborts the whole upload (a failed
                // folder invalidates its subtree), report the specific error and folder.
                for (const auto& newNode: nn)
                {
                    if (newNode.mError != API_OK)
                    {
                        auto it = mUploadIdToTree.find(newNode.nodehandle);
                        LOG_err << "Folder upload failed [" << newNode.mError << "] for folder '"
                                << (it != mUploadIdToTree.end() ? it->second->folderName :
                                                                  std::string("<unknown>"))
                                << "'";
                        complete(newNode.mError);
                        return;
                    }
                }

                // start the next batch, if there are any left (or start transfers, if we are ready)
                vector<NewNode> newnodes;
#ifndef NDEBUG
                batchResult r =
#endif
                    createNextFolderBatch(mUploadTree, newnodes, filecount, true);

                assert(r == batchResult_cancelled || r == batchResult_requestSent ||
                       r == batchResult_batchesComplete || r == batchResult_failed);
            },
            localPitag);

        unsigned existing = 0, total = 0;
        mUploadTree.recursiveCountFolders(existing, total);
        megaApi->fireOnFolderTransferUpdate(transfer, MegaTransfer::STAGE_CREATE_TREE, total, existing, filecount, nullptr, nullptr);

        return batchResult_requestSent;
    }

    if (&tree == &mUploadTree)
    {
        // we recursed the entire tree without finding any more folder nodes to create.
        // time to set the file uploads in motion.

        TransferQueue transferQueue;
        if (!genUploadTransfersForFiles(mUploadTree, transferQueue))
        {
            transferQueue.clear();
            complete(API_EINCOMPLETE, true);
        }
        else if (transferQueue.empty())
        {
            complete(API_OK);
        }
        else
        {
            // once we call sendPendingTransfers, we are guaranteed start/finish callbacks for each file transfer
            // the last callback of onFinish for one of these will also complete and destroy this MegaFolderUploadController
            transfersTotalCount = transferQueue.size();
            megaApi->sendPendingTransfers(&transferQueue, this);
            // no further code can be added here, this object may now be deleted (eg, due to cancel token activation)
        }

        return batchResult_batchesComplete;
    }

    return batchResult_stillRecursing;
}

bool MegaFolderUploadController::genUploadTransfersForFiles(Tree& tree, TransferQueue& transferQueue)
{
    for (const auto& localpath : tree.files)
    {
        MegaApiImpl::MegaUploadOptionsPrivate subOptions;
        subOptions.mPublicOptions.mtime = MegaApi::INVALID_CUSTOM_MOD_TIME;
        subOptions.mFolderTransferTag = tag;
        subOptions.mPublicOptions.appData = transfer->getAppData();
        subOptions.mFsType = tree.fsType;

        MegaTransferPrivate* subTransfer = megaApi->createUploadTransfer(
            localpath.lp,
            tree.megaNode ? tree.megaNode->getHandle() : INVALID_HANDLE,
            subOptions,
            transfer->accessCancelToken(),
            this,
            &localpath.fp);

        Pitag pitag{PitagPurpose::Upload,
                    transfer->getPitag().trigger,
                    PitagNodeType::Folder,
                    transfer->getPitag().target,
                    PitagImportSource::NotApplicable};
        subTransfer->setPitag(pitag);
        transferQueue.push(subTransfer);

        if (isCancelledByFolderTransferToken()) return false;
    }

    for (auto& t : tree.subtrees)
    {
        genUploadTransfersForFiles(*t, transferQueue);
        if (isCancelledByFolderTransferToken()) return false;
    }

    return true;
}

void MegaRecursiveOperation::setRootNodeHandleInTransfer()
{
    if (transfer && transfer->getType() == MegaTransfer::TYPE_UPLOAD)
    {
        // set root folder node handle in MegaTransfer
        LocalPath path = LocalPath::fromAbsolutePath(transfer->getPath());
        auto rootFolderName = transfer->getFileName()
                                  ? transfer->getFileName()
                                  : path.leafName().toPath(true);

        unique_ptr<MegaNode> parentRootNode(megaApi->getNodeByHandle(transfer->getParentHandle()));
        std::unique_ptr<MegaNode>root(megaApi->getChildNode(parentRootNode.get(), rootFolderName.c_str()));
        if (root)
        {
            if (transfer->getNodeHandle() != INVALID_HANDLE
                && transfer->getNodeHandle() != root->getHandle())
            {
                LOG_debug << "setRootNodeHandleInTransfer root nodehandle: " << Base64Str<MegaClient::NODEHANDLE>(root->getHandle())
                          << ": doesn't match with current one: " << Base64Str<MegaClient::NODEHANDLE>(transfer->getNodeHandle()) ;
            }
            transfer->setNodeHandle(root->getHandle());
        }
    }
}

void MegaRecursiveOperation::complete(Error e, bool cancelledByUser)
{
    assert(mMainThreadId == std::this_thread::get_id());

    // Completion only happens on this same thread
    // and only by two possible paths:
    // 1. the very last file sub-transfer has completed
    // 2. there were no files (either by cancel/fail or only folders were needed)
    // for both those cases, the thread must have completed already.
    assert(!mWorkerThread.joinable());

    std::string logMsg = "MegaRecursiveOperation";
    if (cancelledByUser) { logMsg.append(" (has been cancelled by user)"); }
    e ? logMsg.append(" finished with error [").append(std::to_string(e).c_str()).append("]") : logMsg.append(" finished successfully");
    LOG_debug << logMsg << " - bytes: " << transfer->getTransferredBytes() << " of " << transfer->getTotalBytes();

    if (allSubtransfersResolved())
    {
        setRootNodeHandleInTransfer();
    }

    transfer->setState(cancelledByUser ? MegaTransfer::STATE_CANCELLED : MegaTransfer::STATE_COMPLETED);
    megaApi->fireOnTransferFinish(transfer, std::make_unique<MegaErrorPrivate>(e));
}

MegaScheduledCopyController::MegaScheduledCopyController(MegaApiImpl *megaApi, int tag, int folderTransferTag, handle parenthandle, const char* filename, bool attendPastBackups, const char *speriod, int64_t period, int maxBackups)
{
    LOG_info << "Registering backup for folder " << filename << " period=" << period << " speriod=" << speriod << " Number-of-Backups=" << maxBackups;

    this->basepath = filename;
    size_t found = basepath.find_last_of("/\\");
    string aux = basepath;
    while (aux.size() && (found == (aux.size()-1)))
    {
        aux = aux.substr(0, found - 1);
        found = aux.find_last_of("/\\");
    }
    this->backupName = aux.substr((found == string::npos)?0:found+1);

    this->parenthandle = parenthandle;

    this->megaApi = megaApi;
    this->client = megaApi->getMegaClient();

    this->attendPastBackups = attendPastBackups;

    this->pendingTags = 0;

    clearCurrentBackupData();

    lastbackuptime = getLastBackupTime();

    this->backupListener = NULL;

    this->maxBackups = maxBackups;

    this->pendingremovals = 0;

    this->lastwakeuptime = 0;

    this->tag = tag;
    this->folderTransferTag = folderTransferTag;

    valid = true;
    this->setPeriod(period);
    this->setPeriodstring(speriod);
    if (!backupName.size())
    {
        valid = false;
    }

    if (valid)
    {
        megaApi->startTimer(this->startTime - Waiter::ds + 1); //wake the sdk when required
        this->state = MegaScheduledCopy::SCHEDULED_COPY_ACTIVE;
        megaApi->fireOnBackupStateChanged(this);
        removeexceeding(false);
    }
    else
    {
        this->state = MegaScheduledCopy::SCHEDULED_COPY_FAILED;
    }
}

MegaScheduledCopyController::MegaScheduledCopyController(MegaScheduledCopyController *backup)
{
    this->pendingremovals = backup->pendingremovals;
    this->setTag(backup->getTag());
    this->setLocalFolder(backup->getLocalFolder());
    this->setMegaHandle(backup->getMegaHandle());

    this->setFolderTransferTag(backup->getFolderTransferTag());

    this->megaApi = backup->megaApi;
    this->client = backup->client;
    this->setBackupListener(backup->getBackupListener());


    //copy currentBackup data
    this->recursive = backup->recursive;
    this->pendingTransfers = backup->pendingTransfers;
    this->pendingTags = backup->pendingTags;
    for (auto it = backup->pendingFolders.begin(); it != backup->pendingFolders.end(); it++)
    {
        this->pendingFolders.push_back(*it);
    }

    for (std::vector<MegaTransfer *>::iterator it = backup->failedTransfers.begin(); it != backup->failedTransfers.end(); it++)
    {
        this->failedTransfers.push_back(((MegaTransfer *)*it)->copy());
    }
    this->currentHandle = backup->currentHandle;
    this->currentBKStartTime = backup->currentBKStartTime;
    this->updateTime = backup->updateTime;
    this->transferredBytes = backup->transferredBytes;
    this->totalBytes = backup->totalBytes;
    this->speed = backup->speed;
    this->meanSpeed = backup->meanSpeed;
    this->numberFiles = backup->numberFiles;
    this->totalFiles = backup->totalFiles;
    this->numberFolders = backup->numberFolders;
    this->attendPastBackups = backup->attendPastBackups;

    this->offsetds=backup->getOffsetds();
    this->lastbackuptime=backup->getLastBackupTime();
    this->state=backup->getState();
    this->startTime=backup->getStartTime();
    this->period=backup->getPeriod();
    this->ccronexpr=backup->getCcronexpr();
    this->periodstring=backup->getPeriodString();
    this->valid=backup->isValid();

    this->setLocalFolder(backup->getLocalFolder());
    this->setBackupName(backup->getBackupName());
    this->setMegaHandle(backup->getMegaHandle());
    this->setMaxBackups(backup->getMaxBackups());
}


long long MegaScheduledCopyController::getNextStartTime(long long oldStartTimeAbsolute) const
{
    if (oldStartTimeAbsolute == -1)
    {
        return (getNextStartTimeDs() + this->offsetds)/10;
    }
    else
    {
        return (getNextStartTimeDs(oldStartTimeAbsolute*10 - this->offsetds) + this->offsetds)/10;
    }
}

long long MegaScheduledCopyController::getNextStartTimeDs(long long oldStartTimeds) const
{
    if (oldStartTimeds == -1)
    {
        return startTime;
    }
    if (period != -1)
    {
        return oldStartTimeds + period;
    }
    else
    {
        if (!valid)
        {
            return oldStartTimeds;
        }
        long long current_ds = oldStartTimeds + this->offsetds;  // 64 bit

        long long newt = cron_next(&ccronexpr, time_t(current_ds/10));  // time_t is 32 bit still on many systems
        long long newStarTimeds = newt*10-offsetds;  // 64 bit again
        return newStarTimeds;
    }
}

void MegaScheduledCopyController::update()
{
    if (!valid)
    {
        if (!isBusy())
        {
            state = SCHEDULED_COPY_FAILED;
        }
        return;
    }
    if (Waiter::ds > startTime)
    {
        if (!isBusy())
        {
            long long nextStartTime = getNextStartTimeDs(startTime);
            if (nextStartTime <= startTime)
            {
                LOG_err << "Invalid calculated NextStartTime" ;
                valid = false;
                state = SCHEDULED_COPY_FAILED;
                return;
            }

            if (nextStartTime > Waiter::ds)
            {
                start();
            }
            else
            {
                LOG_warn << " BACKUP discarded (too soon, time for the next): " << basepath;
                start(true);
                megaApi->startTimer(1); //wake sdk
            }

            startTime = nextStartTime;
        }
        else
        {
            LOG_verbose << "Backup busy: " << basepath <<
                           ". State=" << ((state==MegaScheduledCopy::SCHEDULED_COPY_ONGOING)?"On Going":"Removing exeeding") << ". Postponing ...";
            if ((lastwakeuptime+10) < Waiter::ds )
            {
                megaApi->startTimer(10); //give it a while
                lastwakeuptime = Waiter::ds+10;
            }
        }
    }
    else
    {
        if (lastwakeuptime < Waiter::ds || ((this->startTime + 1) < lastwakeuptime))
        {
            LOG_debug << " Waking in " << (this->startTime - Waiter::ds + 1) << " deciseconds to do backup";
            megaApi->startTimer(this->startTime - Waiter::ds + 1); //wake the sdk when required
            lastwakeuptime = this->startTime + 1;
        }
    }
}

void MegaScheduledCopyController::removeexceeding(bool currentoneOK)
{
    map<int64_t, MegaNode *> backupTimesNodes;
    int ncompleted=0;

    MegaNode * parentNode = megaApi->getNodeByHandle(parenthandle);

    if (parentNode)
    {
        MegaNodeList* children = megaApi->getChildren(parentNode, MegaApi::ORDER_NONE);
        if (children)
        {
            for (int i = 0; i < children->size(); i++)
            {
                MegaNode *childNode = children->get(i);
                string childname = childNode->getName();

                if (isBackup(childname, backupName) )
                {
                    const char *backstvalue = childNode->getCustomAttr("BACKST");

                    if ( ( !backstvalue || !strcmp(backstvalue,"ONGOING") ) && ( childNode->getHandle() != currentHandle ) )
                    {
                        LOG_err << "Found unexpected ONGOING backup (probably from previous executions). Changing status to MISCARRIED";
                        this->pendingTags++;
                        megaApi->setCustomNodeAttribute(childNode, "BACKST", "MISCARRIED", this);
                    }

                    if ( (backstvalue && !strcmp(backstvalue,"COMPLETE"))
                            || ( childNode->getHandle() == currentHandle && currentoneOK ) //either its completed or is the current one and it went ok (it might not have backstvalue yet set
                            )
                    {
                        ncompleted++;
                    }

                    int64_t timeofbackup = getTimeOfBackup(childname);
                    if (timeofbackup)
                    {
                        backupTimesNodes[timeofbackup]=childNode;
                    }
                    else
                    {
                        LOG_err << "Failed to get backup time for folder: " << childname << ". Discarded.";
                    }
                }
            }
        }
        while (backupTimesNodes.size() > (unsigned int)maxBackups)
        {
            map<int64_t, MegaNode *>::iterator itr = backupTimesNodes.begin();
            const char *backstvalue = itr->second->getCustomAttr("BACKST");
            if ( (ncompleted == 1) && backstvalue && (!strcmp(backstvalue,"COMPLETE")) && backupTimesNodes.size() > 1)
            {
                itr++;
            }

            MegaNode * nodeToDelete = itr->second;
            int64_t timetodelete = itr->first;
            backstvalue = nodeToDelete->getCustomAttr("BACKST");
            if (backstvalue && !strcmp(backstvalue,"COMPLETE"))
            {
                ncompleted--;
            }

            char * nodepath = megaApi->getNodePath(nodeToDelete);
            LOG_info << " Removing exceeding backup " << nodepath;
            delete []nodepath;
            state = SCHEDULED_COPY_REMOVING_EXCEEDING;
            megaApi->fireOnBackupStateChanged(this);
            pendingremovals++;
            megaApi->remove(nodeToDelete, false, this);

            backupTimesNodes.erase(timetodelete);
        }

        delete children;
    }
    delete parentNode;
}

int64_t MegaScheduledCopyController::getLastBackupTime()
{
    map<int64_t, MegaNode *> backupTimesPaths;
    int64_t latesttime=0;

    MegaNode * parentNode = megaApi->getNodeByHandle(parenthandle);
    if (parentNode)
    {
        MegaNodeList* children = megaApi->getChildren(parentNode, MegaApi::ORDER_NONE);
        if (children)
        {
            for (int i = 0; i < children->size(); i++)
            {
                MegaNode *childNode = children->get(i);
                string childname = childNode->getName();
                if (isBackup(childname, backupName) )
                {
                    int64_t timeofbackup = getTimeOfBackup(childname);
                    if (timeofbackup)
                    {
                        backupTimesPaths[timeofbackup]=childNode;
                        latesttime = (std::max)(latesttime, timeofbackup);
                    }
                    else
                    {
                        LOG_err << "Failed to get backup time for folder: " << childname << ". Discarded.";
                    }
                }
            }
            delete children;
        }
        delete parentNode;
    }
    return latesttime;
}

bool MegaScheduledCopyController::isBackup(string localname, string backupname) const
{
    return ( localname.compare(0, backupname.length(), backupname) == 0) && (localname.find("_bk_") != string::npos);
}

int64_t MegaScheduledCopyController::getTimeOfBackup(string localname) const
{
    size_t pos = localname.find("_bk_");
    if (pos == string::npos || ( (pos+4) >= (localname.size()-1) ) )
    {
        return 0;
    }
    string rest = localname.substr(pos + 4).c_str();

//    int64_t toret = atol(rest.c_str());
    int64_t toret = stringToTimestamp(rest, FORMAT_SCHEDULED_COPY);
    return toret;
}

bool MegaScheduledCopyController::getAttendPastBackups() const
{
    return attendPastBackups;
}

MegaTransferList *MegaScheduledCopyController::getFailedTransfers()
{
    MegaTransferList *result = new MegaTransferListPrivate(failedTransfers.data(), int(failedTransfers.size()));
    return result;
}

void MegaScheduledCopyController::setAttendPastBackups(bool value)
{
    attendPastBackups = value;
}

bool MegaScheduledCopyController::isValid() const
{
    return valid;
}

void MegaScheduledCopyController::setValid(bool value)
{
    valid = value;
}

cron_expr MegaScheduledCopyController::getCcronexpr() const
{
    return ccronexpr;
}

void MegaScheduledCopyController::setCcronexpr(const cron_expr &value)
{
    ccronexpr = value;
}

MegaScheduledCopyListener *MegaScheduledCopyController::getBackupListener() const
{
    return backupListener;
}

void MegaScheduledCopyController::setBackupListener(MegaScheduledCopyListener *value)
{
    backupListener = value;
}

long long MegaScheduledCopyController::getTotalFiles() const
{
    return totalFiles;
}

void MegaScheduledCopyController::setTotalFiles(long long value)
{
    totalFiles = value;
}

int64_t MegaScheduledCopyController::getCurrentBKStartTime() const
{
    return currentBKStartTime;
}

void MegaScheduledCopyController::setCurrentBKStartTime(const int64_t &value)
{
    currentBKStartTime = value;
}

int64_t MegaScheduledCopyController::getUpdateTime() const
{
    return updateTime;
}

void MegaScheduledCopyController::setUpdateTime(const int64_t &value)
{
    updateTime = value;
}

long long MegaScheduledCopyController::getTransferredBytes() const
{
    return transferredBytes;
}

void MegaScheduledCopyController::setTransferredBytes(long long value)
{
    transferredBytes = value;
}

long long MegaScheduledCopyController::getTotalBytes() const
{
    return totalBytes;
}

void MegaScheduledCopyController::setTotalBytes(long long value)
{
    totalBytes = value;
}

long long MegaScheduledCopyController::getSpeed() const
{
    return speed;
}

void MegaScheduledCopyController::setSpeed(long long value)
{
    speed = value;
}

long long MegaScheduledCopyController::getMeanSpeed() const
{
    return meanSpeed;
}

void MegaScheduledCopyController::setMeanSpeed(long long value)
{
    meanSpeed = value;
}

long long MegaScheduledCopyController::getNumberFiles() const
{
    return numberFiles;
}

void MegaScheduledCopyController::setNumberFiles(long long value)
{
    numberFiles = value;
}

long long MegaScheduledCopyController::getNumberFolders() const
{
    return numberFolders;
}

void MegaScheduledCopyController::setNumberFolders(long long value)
{
    numberFolders = value;
}

int64_t MegaScheduledCopyController::getLastbackuptime() const
{
    return lastbackuptime;
}

void MegaScheduledCopyController::setLastbackuptime(const int64_t &value)
{
    lastbackuptime = value;
}

void MegaScheduledCopyController::setState(int value)
{
    state = value;
}

bool MegaScheduledCopyController::isBusy() const
{
    return (state == SCHEDULED_COPY_ONGOING) || (state == SCHEDULED_COPY_REMOVING_EXCEEDING || (state == SCHEDULED_COPY_SKIPPING));
}

std::string MegaScheduledCopyController::epochdsToString(const int64_t rawtimeds) const
{
    struct tm dt;
    char buffer [40];
    time_t rawtime = rawtimeds/10;
    m_localtime(rawtime, &dt);

    strftime(buffer, sizeof( buffer ), "%Y%m%d%H%M%S", &dt);

    return std::string(buffer);
}

void MegaScheduledCopyController::clearCurrentBackupData()
{
    this->recursive = 0;
    this->pendingTransfers = 0;
    this->pendingFolders.clear();
    for (std::vector<MegaTransfer *>::iterator it = failedTransfers.begin(); it != failedTransfers.end(); it++)
    {
        delete *it;
    }
    this->failedTransfers.clear();
    this->currentHandle = UNDEF;
    this->currentBKStartTime = 0;
    this->updateTime = 0;
    this->transferredBytes = 0;
    this->totalBytes = 0;
    this->speed = 0;
    this->meanSpeed = 0;
    this->numberFiles = 0;
    this->totalFiles = 0;
    this->numberFolders = 0;
}


void MegaScheduledCopyController::start(bool skip)
{
    LOG_info << "starting backup of " << basepath << ". Next one will be in " << getNextStartTimeDs(startTime)-offsetds << " ds" ;
    clearCurrentBackupData();
    this->setCurrentBKStartTime(Waiter::ds); //notice: this is != StarTime

    size_t plastsep = basepath.find_last_of("\\/");
    if(plastsep == string::npos)
        plastsep = size_t(-1);
    string name = basepath.substr(plastsep+1);

    std::ostringstream ossremotename;
    ossremotename << name;
    ossremotename << "_bk_";
    ossremotename << epochdsToString(offsetds+startTime);
    string backupname = ossremotename.str();
    currentName = backupname;

    lastbackuptime = (std::max)(lastbackuptime,offsetds+startTime);

    megaApi->fireOnBackupStart(this);

    MegaNode *parent = megaApi->getNodeByHandle(parenthandle);
    if(!parent)
    {
        LOG_err << "Could not start backup: "<< name << ". Parent node not found";
        megaApi->fireOnBackupFinish(this, std::make_unique<MegaErrorPrivate>(API_ENOENT));

    }
    else
    {
        if (skip)
        {
            state = SCHEDULED_COPY_SKIPPING;
        }
        else
        {
            state = SCHEDULED_COPY_ONGOING;
        }
        megaApi->fireOnBackupStateChanged(this);

        auto localpath = LocalPath::fromAbsolutePath(basepath);

        MegaNode *child = megaApi->getChildNode(parent, backupname.c_str());

        if(!child || !child->isFolder())
        {
            pendingFolders.push_back(localpath);
            megaApi->createFolder(backupname.c_str(), parent, this);
        }
        else
        {
            LOG_err << "Could not start backup: "<< backupname << ". Backup already exists";
            megaApi->fireOnBackupFinish(this, std::make_unique<MegaErrorPrivate>(API_EEXIST));
            state = SCHEDULED_COPY_ACTIVE;

        }

        delete child;
        delete parent;
    }
}

void MegaScheduledCopyController::onFolderAvailable(MegaHandle handle)
{
    MegaNode *parent = megaApi->getNodeByHandle(handle);
    if(currentHandle == UNDEF)//main folder of the backup instance
    {
        currentHandle = handle;
        if (state == SCHEDULED_COPY_ONGOING)
        {
            this->pendingTags++;
            megaApi->setCustomNodeAttribute(parent, "BACKST", "ONGOING", this);
        }
        else
        {
            this->pendingTags++;
            megaApi->setCustomNodeAttribute(parent, "BACKST", "SKIPPED", this);
        }
    }
    else
    {
        numberFolders++;
    }
    recursive++;
    LocalPath localPath = pendingFolders.front();
    pendingFolders.pop_front();

    if (state == SCHEDULED_COPY_ONGOING)
    {
        LocalPath localname;
        auto da = client->fsaccess->newdiraccess();
        if (da->dopen(&localPath, NULL, false))
        {
            FileSystemType fsType = client->fsaccess->getlocalfstype(localPath);

            LocalPath childPath = localPath;
            while (da->dnext(childPath, localname, false))
            {
                childPath.appendWithSeparator(localname, false);

                //TODO: add exclude filters here

                auto fa = client->fsaccess->newfileaccess();
                if (fa->fopen(childPath, OPEN_RDONLY, FSLogging::logOnError))
                {
                    string name = localname.toName(*client->fsaccess);
                    if(fa->type == FILENODE)
                    {
                        pendingTransfers++;

                        totalFiles++;
                        const auto parentNode =
                            client->mNodeManager.getNodeByHandle(NodeHandle().set6byte(handle));

                        const bool inIncomingShare =
                            parentNode && parentNode->matchesOrHasAncestorMatching(
                                              [](const Node& node)
                                              {
                                                  return node.inshare != nullptr;
                                              });

                        const PitagTarget pitagTarget =
                            inIncomingShare ? PitagTarget::IncomingShare : PitagTarget::CloudDrive;

                        MegaApiImpl::MegaUploadOptionsPrivate uploadOptions;
                        uploadOptions.mPublicOptions.mtime = -1;
                        uploadOptions.mFolderTransferTag = folderTransferTag;
                        uploadOptions.mIsBackup = true;
                        uploadOptions.mFsType = fsType;
                        uploadOptions.mPitagTarget = pitagTarget;

                        const std::string childPathStr = childPath.toPath(false);
                        megaApi->startUpload(childPathStr,
                                             parent ? parent->getHandle() : INVALID_HANDLE,
                                             CancelToken(),
                                             uploadOptions,
                                             this);
                    }
                    else
                    {
                        MegaNode *child = megaApi->getChildNode(parent, name.c_str());
                        if(!child || !child->isFolder())
                        {
                            pendingFolders.push_back(childPath);
                            megaApi->createFolder(name.c_str(), parent, this);
                        }
                        else
                        {
                            pendingFolders.push_front(childPath);
                            onFolderAvailable(child->getHandle());
                        }
                        delete child;
                    }
                }

                childPath = localPath;
            }
        }
    }
    else if (state == SCHEDULED_COPY_SKIPPING)
    {
        //do nth
    }
    else
    {
        LOG_warn << " Backup folder created while not ONGOING: " << localPath;
    }

    delete parent;
    recursive--;

    checkCompletion();
}

bool MegaScheduledCopyController::checkCompletion()
{
    if(!recursive && !pendingFolders.size() && !pendingTransfers && !pendingTags)
    {
        error e = API_OK;
        LOG_debug << "Folder transfer finished - " << this->getTransferredBytes() << " of " << this->getTotalBytes();
        MegaNode *node = megaApi->getNodeByHandle(currentHandle);
        if (node)
        {
            if (failedTransfers.size())
            {
                this->pendingTags++;
                megaApi->setCustomNodeAttribute(node, "BACKST", "INCOMPLETE", this);
                e = API_EINCOMPLETE;
            }
            else if (state != SCHEDULED_COPY_SKIPPING)
            {
                this->pendingTags++;
                megaApi->setCustomNodeAttribute(node, "BACKST", "COMPLETE", this);
            }
            else
            {
                e = API_EEXPIRED;
            }
            delete node;
        }
        else
        {
            LOG_err << "Could not set backup attribute, node not found for: " << currentName;
            e = API_ENOENT;
        }

        state = SCHEDULED_COPY_ACTIVE;
        megaApi->fireOnBackupFinish(this, std::make_unique<MegaErrorPrivate>(e));
        megaApi->fireOnBackupStateChanged(this);

        removeexceeding(e == API_OK);

        return true;
    }
    return false;
}

int MegaScheduledCopyController::getFolderTransferTag() const
{
    return folderTransferTag;
}

void MegaScheduledCopyController::setFolderTransferTag(int value)
{
    folderTransferTag = value;
}

int64_t MegaScheduledCopyController::getOffsetds() const
{
    return offsetds;
}

void MegaScheduledCopyController::setOffsetds(const int64_t &value)
{
    offsetds = value;
}

void MegaScheduledCopyController::abortCurrent()
{
    LOG_debug << "Setting backup as aborted: " << currentName;

    if (state == SCHEDULED_COPY_ONGOING || state == SCHEDULED_COPY_SKIPPING)
    {
        megaApi->fireOnBackupFinish(this, std::make_unique<MegaErrorPrivate>(API_EINCOMPLETE));
    }

    state = SCHEDULED_COPY_ACTIVE;
    megaApi->fireOnBackupStateChanged(this);

    MegaNode *node = megaApi->getNodeByHandle(currentHandle);
    if (node)
    {
        this->pendingTags++;
        megaApi->setCustomNodeAttribute(node, "BACKST", "ABORTED", this);
        delete node;
    }
    else
    {
        LOG_err << "Could not set backup attribute, node not found for: " << currentName;
    }

    clearCurrentBackupData();

}

void MegaScheduledCopyController::onRequestFinish(MegaApi *, MegaRequest *request, MegaError *e)
{
    int type = request->getType();
    int errorCode = e->getErrorCode();

    if(type == MegaRequest::TYPE_CREATE_FOLDER)
    {
        if(!errorCode)
        {
            onFolderAvailable(request->getNodeHandle());
            megaApi->fireOnBackupUpdate(this);
        }
        else
        {
            pendingFolders.pop_front();
            megaApi->fireOnBackupUpdate(this);
            checkCompletion();
        }
    }
    else if(type == MegaRequest::TYPE_REMOVE)
    {
        pendingremovals--;
        if (!pendingremovals)
        {
            assert(pendingTags>=0);
            if (pendingTags <= 0)
            {
                state = SCHEDULED_COPY_ACTIVE;
            }
            megaApi->fireOnBackupStateChanged(this);
        }
    }
    else if(type == MegaRequest::TYPE_SET_ATTR_NODE)
    {
        pendingTags--;
        assert(pendingTags>=0);

        if (!pendingTags)
        {
            if (state == SCHEDULED_COPY_ONGOING || state == SCHEDULED_COPY_SKIPPING)
            {
                checkCompletion();
            }
            else // from REMOVING OR after abort
            {
                if (state != SCHEDULED_COPY_ACTIVE)
                {
                    state = SCHEDULED_COPY_ACTIVE;
                    megaApi->fireOnBackupStateChanged(this);
                }
            }
        }
    }

}

void MegaScheduledCopyController::onTransferStart(MegaApi *, MegaTransfer *t)
{
    LOG_verbose << " at MegaScheduledCopyController::onTransferStart: "+ string(t->getFileName());

    this->setTotalBytes(this->getTotalBytes() + t->getTotalBytes());
    this->setUpdateTime(Waiter::ds);

    megaApi->fireOnBackupUpdate(this);
}

void MegaScheduledCopyController::onTransferUpdate(MegaApi *, MegaTransfer *t)
{
    LOG_verbose << " at MegaScheduledCopyController::onTransferUpdate";

    this->setTransferredBytes(this->getTransferredBytes() + t->getDeltaSize());
    this->setUpdateTime(Waiter::ds);
    this->setSpeed(t->getSpeed());
    this->setMeanSpeed(t->getMeanSpeed());

    megaApi->fireOnBackupUpdate(this);
}

void MegaScheduledCopyController::onTransferTemporaryError(MegaApi*, MegaTransfer*, MegaError* e)
{
    LOG_verbose << " at MegaScheduledCopyController::onTransferTemporaryError";

    unique_ptr<MegaErrorPrivate> errorPrivate;
    if (dynamic_cast<MegaErrorPrivate *>(e))
    {
        errorPrivate = unique_ptr<MegaErrorPrivate>(dynamic_cast<MegaErrorPrivate *>(e->copy()));
    }
    else
    {
         errorPrivate = std::make_unique<MegaErrorPrivate>(e->getErrorCode());
    }

    megaApi->fireOnBackupTemporaryError(this, std::move(errorPrivate));  // we received a non-owning pointer but we need to pass ownership to fireOnBackupTemporaryError
}

void MegaScheduledCopyController::onTransferFinish(MegaApi *, MegaTransfer *t, MegaError *e)
{
    LOG_verbose << " at MegaackupController::onTransferFinish";

    pendingTransfers--;
//    this->setTransferredBytes(this->getTransferredBytes() + t->getDeltaSize()); //TODO: THIS was in MegaUploaderController (which seems wrong)
    this->setUpdateTime(Waiter::ds);
    this->setSpeed(t->getSpeed());
    this->setMeanSpeed(t->getMeanSpeed());

    if (e->getErrorCode() != MegaError::API_OK)
    {
        failedTransfers.push_back(t->copy());
    }
    else
    {
        numberFiles++;
    }

    megaApi->fireOnBackupUpdate(this);

    checkCompletion();
}

MegaScheduledCopy *MegaScheduledCopyController::copy()
{
    return new MegaScheduledCopyController(this);
}


int MegaScheduledCopyController::getMaxBackups() const
{
    return maxBackups;
}

void MegaScheduledCopyController::setMaxBackups(int value)
{
    maxBackups = value;
}

string MegaScheduledCopyController::getBackupName() const
{
    return backupName;
}

void MegaScheduledCopyController::setBackupName(const string &value)
{
    backupName = value;
}

int64_t MegaScheduledCopyController::getPeriod() const
{
    return period;
}

const char *MegaScheduledCopyController::getPeriodString() const
{
    return periodstring.c_str();
}

void MegaScheduledCopyController::setPeriod(const int64_t &value)
{
    period = value;

    if (value != -1)
    {
        int64_t ds = Waiter::ds.load();
        offsetds   = m_time(NULL) * 10 - ds;
        startTime  = lastbackuptime ? (lastbackuptime + period - offsetds) : ds;
        startTime  = std::max(startTime, ds);
    }
}

void MegaScheduledCopyController::setPeriodstring(const string &value)
{
    periodstring = value;
    valid = true;
    if (value.size())
    {
        const char* err = NULL;
        memset((cron_expr *)&ccronexpr, 0, sizeof(ccronexpr));
        cron_parse_expr(periodstring.c_str(), &ccronexpr, &err);

        if (err != NULL)
        {
            valid = false;
            return;
        }

        this->offsetds=m_time(NULL)*10 - Waiter::ds;

        if (!lastbackuptime)
        {
            this->startTime = Waiter::ds;
        }
        else
        {
            this->startTime = this->getNextStartTimeDs(lastbackuptime-offsetds);
        }
        if (this->startTime < Waiter::ds)
        {
            //to avoid skipping (do empty backups with SKIPPED attr) for a long while (e.g: period too short or downtime too long)
            // we determine a max number of executions to skip.

            int maxBackupToSkip = maxBackups + 10;
            int64_t* starttimes = new int64_t[static_cast<size_t>(maxBackupToSkip)];
            int64_t next = lastbackuptime-offsetds;
            int64_t previousnext = next;

            for (int i = 0; i < maxBackupToSkip; i++)
            {
                starttimes[i] = startTime;
            }

            int j = 0;

            do
            {
                previousnext = next;
                next = this->getNextStartTimeDs(next);
                starttimes[j] = next;
                j = (j==(maxBackupToSkip-1))?0:j+1;
            } while (next > previousnext && next < Waiter::ds);

            if (!attendPastBackups)
            {
                this->startTime = next;
            }
            else
            {
                this->startTime = starttimes[j]; //starttimes[j] should have the oldest time
            }
            delete [] starttimes;
        }
        LOG_debug << " Next Backup set in " << startTime - Waiter::ds << " deciseconds. At: " << epochdsToString((this->startTime+this->offsetds));
    }
}

int64_t MegaScheduledCopyController::getStartTime() const
{
    return startTime;
}

void MegaScheduledCopyController::setStartTime(const int64_t &value)
{
    startTime = value;
}

int MegaScheduledCopyController::getTag() const
{
    return this->tag;
}

void MegaScheduledCopyController::setTag(int value)
{
    tag = value;
}

MegaHandle MegaScheduledCopyController::getMegaHandle() const
{
    return this->parenthandle;
}

void MegaScheduledCopyController::setMegaHandle(const MegaHandle &value)
{
    parenthandle = value;
}

const char *MegaScheduledCopyController::getLocalFolder() const
{
    return this->basepath.c_str();
}

void MegaScheduledCopyController::setLocalFolder(const string &value)
{
    basepath = value;
}

int MegaScheduledCopyController::getState() const
{
    return state;
}

MegaScheduledCopyController::~MegaScheduledCopyController()
{
    megaApi->removeRequestListener(this);
    megaApi->removeTransferListener(this);

    for (std::vector<MegaTransfer *>::iterator it = failedTransfers.begin(); it != failedTransfers.end(); it++)
    {
        delete *it;
    }
}

// Note: it's mandatory to notify stage change from MegaApiImpl's thread
void MegaRecursiveOperation::notifyStage(uint8_t stage)
{
    // Make a copy of transfer and set stage in the temp transfer.
    // If we set stage in the original transfer, we may incur in a wrong order of notification.
    assert(mMainThreadId == std::this_thread::get_id());
    assert (stage > MegaTransfer::STAGE_NONE && stage <= MegaTransfer::STAGE_MAX);
    LOG_debug << "MegaRecursiveOperation: starting " << MegaTransfer::stageToString(stage);
    unique_ptr<MegaTransfer> tempTransfer(transfer->copy());
    MegaTransferPrivate *transferPtr = static_cast<MegaTransferPrivate*>(tempTransfer.get());
    transferPtr->setStage(stage);
    megaApi->fireOnTransferUpdate(transferPtr);
}

void MegaRecursiveOperation::ensureThreadStopped()
{
    mWorkerThreadStopFlag = true;
    if (mWorkerThread.joinable())
    {
        mWorkerThread.join();
    }
}

bool MegaRecursiveOperation::isCancelledByFolderTransferToken() const
{
    return transfer->accessCancelToken().isCancelled();
}

MegaClient* MegaRecursiveOperation::megaapiThreadClient()
{
    assert(mMainThreadId == std::this_thread::get_id());
    return mMegaapiThreadClient;
}

MegaFolderDownloadController::MegaFolderDownloadController(MegaApiImpl* api,
                                                           MegaTransferPrivate* t):
    MegaRecursiveOperation(api->client)
{
    fsaccess = mega::createFSA();
    megaApi = api;
    fsaccess->setdefaultfilepermissions(megaApi->getDefaultFilePermissions()); // Grant default file permissions
    fsaccess->setdefaultfolderpermissions(megaApi->getDefaultFolderPermissions()); // Grant default folder permissions
    transfer = t;
    listener = t->getListener();
    recursive = 0;
    tag = t->getTag();
    mMainThreadId = std::this_thread::get_id();
}

MegaFolderDownloadController::~MegaFolderDownloadController()
{
    assert(mMainThreadId == std::this_thread::get_id());
    LOG_debug << "MegaFolderDownloadController dtor is being called from main thread";
    ensureThreadStopped();
}

void MegaFolderDownloadController::start(MegaNode *node)
{
    assert(mMainThreadId == std::this_thread::get_id());
    transfer->setFolderTransferTag(-1);
    transfer->setStartTime(Waiter::ds);
    transfer->setState(MegaTransfer::STATE_QUEUED);
    megaApi->fireOnTransferStart(transfer);

    unique_ptr<MegaNode> autoDelNode;
    if (!node)
    {
        node = megaApi->getNodeByHandle(transfer->getNodeHandle());
        autoDelNode.reset(node);
        if (!node)
        {
            LOG_debug << "Folder download failed. Node not found";
            complete(API_ENOENT);
            return;
        }
    }
    LocalPath path;
    if (transfer->getParentPath())
    {
        path = LocalPath::fromAbsolutePath(transfer->getParentPath());
    }
    else
    {
        path = LocalPath::fromAbsolutePath(".");
        path.appendWithSeparator(LocalPath::fromRelativePath(""), true);
    }

    FileSystemType fsType = megaapiThreadClient()->fsaccess->getlocalfstype(path);
    const LocalPath &name = (!transfer->getFileName() || !strlen(transfer->getFileName()))
         ? LocalPath::fromRelativeName(node->getName(), *megaapiThreadClient()->fsaccess, fsType)
         : LocalPath::fromRelativeName(transfer->getFileName(), *megaapiThreadClient()->fsaccess, fsType);

    path.appendWithSeparator(name, true);
    transfer->setLocalPath(path);

    // check we are not overwriting file with folder:
    auto tmpfileaccess = fsaccess->newfileaccess();
    if (tmpfileaccess->isfile(path))
    {
        // destination path could not be open or there's a node (TYPE_FILE) with the same name in the destination path
        complete(API_EEXIST);
        return;
    }

    notifyStage(MegaTransfer::STAGE_SCAN);
    // for download scan is just checking nodes, we can do this all in one quick pass
    unsigned fileAddedCount = 0;
    scanFolder_result sr = scanFolder(node, path, fsType, fileAddedCount);

    if (sr != scanFolder_succeeded)
    {
        if (sr == scanFolder_cancelled)
        {
            complete(API_EINCOMPLETE, true);
        }
        else // inconsistent node state
        {
            complete(API_EINTERNAL);
        }
    }
    else
    {
        // it's mandatory to notify stage change from MegaApiImpl's thread to avoid deadlocks and other issues
        notifyStage(MegaTransfer::STAGE_CREATE_TREE);

        // start worker thread to create local folder tree
        mWorkerThread = std::thread([this, fsType, path, fileAddedCount](){

            // local folder creation runs on the download worker thread (and checks the cancelled flag)
            Error e;
            std::shared_ptr<TransferQueue> transferQueue = createFolderGenDownloadTransfersForFiles(fsType, fileAddedCount, e);

            // mCompletionForMegaApiThread lambda will be executed on the MegaApiImpl's thread
            // use a weak_ptr in case this 'this' object doesn't exist anymore when lambda starts executing
            weak_ptr<MegaFolderDownloadController> weak_this = shared_from_this();

            // the thread always queues a function to execute on MegaApi thread for onFinish()
            // we keep a pointer to it in case we need to cancel()
            mCompletionForMegaApiThread.reset(new ExecuteOnce([this, e, transferQueue, path, weak_this]() {

                // double check our object still exists when completion function starts executing
                if (!weak_this.lock()) return;
                assert(weak_this.lock().get() == this);

                // these next parts must run on MegaApiImpl's thread again, as
                // genUploadTransfersForFiles or checkCompletion may call the fireOnXYZ() functions
                assert(mMainThreadId == std::this_thread::get_id());

                // make sure the thread is joined.  This lets us add error-catching asserts elsewhere.
                if (mWorkerThread.joinable())
                {
                    mWorkerThread.join();
                }

                if (e)
                {
                    if (transferQueue)
                    {
                        transferQueue->clear();
                    }
                    complete(e);
                }
                else
                {
                    // downloadFiles must run on the megaApi thread, as it may call fireOnTransferXYZ()
                    if (!transferQueue)
                    {
                        complete(API_EINCOMPLETE, true);
                    }
                    else if (transferQueue->empty())
                    {
                        complete(API_OK);
                    }
                    else
                    {
                        // once we call sendPendingTransfers, we are guaranteed start/finish callbacks for each file transfer
                        // the last callback of onFinish for one of these will also complete and destroy this MegaFolderUploadController
                        transfersTotalCount = transferQueue->size();

                        megaApi->sendPendingTransfers(transferQueue.get(), this, megaapiThreadClient()->fsaccess->availableDiskSpace(path));
                        // no further code can be added here, this object may now be deleted (eg, due to cancel token activation)

                        // complete() will finally be called when the last sub-transfer finishes
                    }
                }
            }));

            // Queue that function.
            megaApi->executeOnThread(mCompletionForMegaApiThread);
        });
    }
}

MegaFolderDownloadController::scanFolder_result MegaFolderDownloadController::scanFolder(MegaNode *node, LocalPath& localpath, FileSystemType fsType, unsigned& fileAddedCount)
{
    assert(mMainThreadId == std::this_thread::get_id());

    if (isCancelledByFolderTransferToken())
    {
        return scanFolder_cancelled;
    }

    recursive++;
    size_t index = 0;
    if (node->getType() == FOLDERNODE || node->getType() == ROOTNODE)
    {
       // If node is a folder or root node, store it's localPath, along with a vector with it's children file nodes
       mLocalTree.emplace_back(LocalTree(localpath));
       index = mLocalTree.size() - 1;
    }

    megaApi->fireOnFolderTransferUpdate(transfer, MegaTransfer::STAGE_SCAN, unsigned(mLocalTree.size()), 0, fileAddedCount, &localpath, nullptr);

    MegaNodeList *children = nullptr;
    unique_ptr<MegaNodeList> autoDelChildren;
    if (node->isForeign())
    {
        children = node->getChildren();
    }
    else
    {
        children = megaApi->getChildren(node, MegaApi::ORDER_NONE);  // no order is much faster for a very large folder (or nested folders with large subfolders)
        autoDelChildren.reset(children);
    }

    if (!children)
    {
        LOG_err << "Child nodes not found: " << localpath;
        recursive--;
        return scanFolder_failed;
    }

    for (int i = 0; i < children->size(); i++)
    {
        if (isCancelledByFolderTransferToken())
        {
            return scanFolder_cancelled;
        }

        MegaNode *child = children->get(i);
        if (child->getType() == MegaNode::TYPE_FILE)
        {
            // Add child node to vector in mLocalTree at index we have stored it's localPath
            ::mega::unique_ptr<MegaNode> childNode (child->copy());
            mLocalTree.at(index).childrenNodes.push_back(std::move(childNode));
            fileAddedCount += 1;
        }
        else
        {
            LocalPath newpath{localpath};
            newpath.appendWithSeparator(
                LocalPath::fromRelativeName(child->getName(), *fsaccess, fsType),
                true);
            scanFolder_result result = scanFolder(child, newpath, fsType, fileAddedCount);

            if (result != scanFolder_succeeded)
            {
                recursive--;
                return result;
            }
        }
    }
    recursive--;
    return scanFolder_succeeded;
}

bool MegaRecursiveOperation::isStoppedOrCancelled(const std::string& name) const
{
    if (mWorkerThreadStopFlag)
    {
        LOG_debug << name << " thread stopped by flag";
        return true;
    }
    if (isCancelledByFolderTransferToken())
    {
        LOG_debug << name << " thread stopped by cancel token";
        return true;
    }
    return false;
}

// Create all local directories in one shot (on the download worker thread)
// for performance and reducing UI waiting time, we combine createFolder and transferQueue generating in one loop
std::unique_ptr<TransferQueue> MegaFolderDownloadController::createFolderGenDownloadTransfersForFiles(FileSystemType fsType, uint32_t fileCount, Error &e)
{
    unsigned created = 0;
    assert(mMainThreadId != std::this_thread::get_id());

    auto transferQueue = std::make_unique<TransferQueue>();

    // update stage to begin
    if (!mLocalTree.empty())
    {
        megaApi->fireOnFolderTransferUpdate(transfer, MegaTransfer::STAGE_CREATE_TREE, unsigned(mLocalTree.size()), created, fileCount, nullptr, nullptr);
    }

    // creating folders and generate transfers for files
    auto it = mLocalTree.begin();
    while (it != mLocalTree.end())
    {
        if (isStoppedOrCancelled("MegaFolderDownloadController::createFolderGenDownloadTransfersForFiles"))
        {
            e = API_EINCOMPLETE;
            return transferQueue;
        }

        LocalPath &localpath = it->localPath;

        auto collistionResolution = transfer->getCollisionResolution();
        // try to create the folder
        e = MegaApiImpl::createLocalFolder_unlocked(localpath, *fsaccess, collistionResolution);

        // errors besides the folder already exists is an error
        if (e && e != API_EEXIST)
        {
            mLocalTree.clear();
            return transferQueue;
        }

        auto folderAlreadyExist = (e && e == API_EEXIST);

        if (!genDownloadTransfersForFiles(transferQueue.get(), *it, fsType, folderAlreadyExist))
        {
            e = API_EINCOMPLETE;
            return transferQueue;
        }

        ++it;
        ++created;

        megaApi->fireOnFolderTransferUpdate(transfer, MegaTransfer::STAGE_CREATE_TREE, unsigned(mLocalTree.size()), created, fileCount, nullptr, nullptr);
    }

    e = API_OK;
    return transferQueue;
}

bool MegaFolderDownloadController::genDownloadTransfersForFiles(
    TransferQueue* transferQueue,
    LocalTree& folder,
    FileSystemType fsType,
    bool folderExists)
{
    assert(transferQueue);

    for (auto& fileNode : folder.childrenNodes)
    {
        if (isStoppedOrCancelled("MegaFolderDownloadController::genDownloadTransfersForFiles"))
        {
            return false;
        }

        // get file local path
        auto fileLocalPath = folder.localPath;
        fileLocalPath.appendWithSeparator(LocalPath::fromRelativeName(fileNode->getName(), *fsaccess, fsType), true);

        auto decision = CollisionChecker::Result::Download;

        // collision might exist only if the folder already exists
        if (folderExists)
        {
            auto fa = fsaccess->newfileaccess();
            if (fa && fa->fopen(fileLocalPath, OPEN_RDONLY, FSLogging::logExceptFileNotFound) &&
                fa->type == FILENODE)
            {
                decision = CollisionChecker::check(fsaccess.get(), fileLocalPath, fileNode.get(), transfer->getCollisionCheck());
            }
        }

        MegaTransferPrivate* transferDownload =
            megaApi->createDownloadTransfer(false,
                                            fileNode.get(),
                                            fileLocalPath,
                                            nullptr,
                                            tag,
                                            transfer->getAppData(),
                                            transfer->accessCancelToken(),
                                            static_cast<int>(transfer->getCollisionCheck()),
                                            static_cast<int>(transfer->getCollisionResolution()),
                                            transfer->getNodeToUndelete() != nullptr,
                                            this,
                                            fsType);

        transferDownload->setCollisionCheckResult(decision);

        transferQueue->push(transferDownload);
    }

    return true;
}


#ifdef HAVE_LIBUV
StreamingBuffer::StreamingBuffer(const std::string& logName):
    logname{logName}
{
    this->capacity = 0;
    this->buffer = NULL;
    this->inpos = 0;
    this->outpos = 0;
    this->size = 0;
    this->free = 0;
    this->maxBufferSize = MAX_BUFFER_SIZE;
    this->maxOutputSize = MAX_OUTPUT_SIZE;
    this->fileSize = 0;
    this->duration = 0;
}

StreamingBuffer::~StreamingBuffer()
{
    delete [] buffer;
}

void StreamingBuffer::init(size_t newCapacity)
{
    assert(newCapacity > 0);
    // Recalculate maxBufferSize and maxOutputSize.
    calcMaxBufferAndMaxOutputSize();
    // Truncate new capacity if needed
    if (newCapacity > maxBufferSize)
    {
        const auto bytesPerSecond = static_cast<size_t>(getBytesPerSecond());
        LOG_warn
            << getLogName()
            << "[Streaming] Truncating requested capacity due to being greater than maxBufferSize."
            << " Capacity requested = " << newCapacity << " bytes"
            << ", truncated to = " << maxBufferSize << " bytes"
            << " [file size = " << fileSize << " bytes"
            << ", total duration = "
            << (duration ? (std::to_string(duration).append(" secs")) : "not a media file")
            << (duration ? std::string(", estimated duration in truncated buffer: ")
                               .append(std::to_string(maxBufferSize / bytesPerSecond))
                               .append(" secs")
                               .append(", max length to be served: ")
                               .append(std::to_string(maxOutputSize / bytesPerSecond))
                               .append(" secs")
                               .append(", bytes per second: ")
                               .append(std::to_string(bytesPerSecond / 1024))
                               .append(" KB/s") :
                           "")
            << "]";
        newCapacity = maxBufferSize;
    }
    else
    {
        LOG_debug << getLogName() << "[Streaming] Init StreamingBuffer."
                  << " Capacity requested = " << newCapacity << " bytes"
                  << " [file size = " << fileSize << " bytes"
                  << ", total duration = "
                  << (duration ? (std::to_string(duration).append(" secs")) : "not a media file")
                  << (duration ? std::string(", estimated duration in buffer: ")
                                     .append(std::to_string(
                                                 partialDuration(static_cast<m_off_t>(newCapacity)))
                                                 .append(" secs")) :
                                 "")
                  << "]";
    }

    capacity = static_cast<unsigned>(newCapacity);
    this->buffer = new char[capacity];
    this->inpos = 0;
    this->outpos = 0;
    this->size = 0;
    this->free = capacity;
}

void StreamingBuffer::calcMaxBufferAndMaxOutputSize()
{
    size_t maxReadChunkSize = static_cast<size_t>(DirectReadSlot::MAX_DELIVERY_CHUNK);
#if defined(__ANDROID__) || defined(USE_IOS)
    size_t minNeededBufferSize = (maxReadChunkSize * 5) / 2; // Equivalent to x2.5 [x * (2 + 1/2) = x * 5/2]
#else
    constexpr size_t BUFFER_PAUSE_RESUME_FACTOR = 3; // Take into account pausing/resuming actions: we need that margin.
    size_t minNeededBufferSize = maxReadChunkSize * BUFFER_PAUSE_RESUME_FACTOR;
#endif
    size_t maxDeliveryChunksPerByteRate;
    if (duration)
    {
        const auto bytesPerSecond = static_cast<size_t>(getBytesPerSecond());
        // Desirable min buffer capacity: 10 seconds at least. Limit: 2x max buffer size requested by the client.
        size_t minDesirableBufferSize = std::min(10 * bytesPerSecond, maxBufferSize * 2);
        // Min buffer capacity needed to prevent data from shrinking: minNeededBufferSize at least.
        minNeededBufferSize = std::max(minNeededBufferSize, minDesirableBufferSize);
        // Calc multiplication factor for maxOutputSize depending on the byteRate and the maxReadChunkSize from DirectReadSlot.
        maxDeliveryChunksPerByteRate = std::max<size_t>((bytesPerSecond / maxReadChunkSize) + ((bytesPerSecond % maxReadChunkSize != 0) ? 1 : 0), 1ul);
    }
    else
    {
        // Default multiplication factor for maxOutputSize (there is no byteRate to adapt the value).
        maxDeliveryChunksPerByteRate = 1;
    }
     // Set maxBufferSize taking into account minNeededBufferSize, truncating the value to be divisible by maxReadChunkSize.
    maxBufferSize = (std::max(maxBufferSize, minNeededBufferSize) / maxReadChunkSize) * maxReadChunkSize;
    // Set max outputSize depending on maxDeliveryChunksPerByteRate. Limit is maxBufferSize.
    maxOutputSize = std::min(maxDeliveryChunksPerByteRate * maxReadChunkSize, maxBufferSize);
}

void StreamingBuffer::reset(bool freeData, size_t sizeToReset)
{
    if (!sizeToReset || sizeToReset > size)
    {
        sizeToReset = size;
    }
    LOG_warn << getLogName() << "[Streaming] Reset streaming buffer. Actual size: " << size
             << ", free: " << free << ", capacity = " << capacity
             << ", size to reset: " << sizeToReset << "] [inpos = " << inpos
             << ", outpos = " << outpos << "]";
    this->inpos = this->inpos >= sizeToReset ?
                        this->inpos - sizeToReset :
                        this->capacity - (sizeToReset - this->inpos);
    this->outpos = this->outpos >= sizeToReset ?
                        this->outpos - sizeToReset :
                        this->capacity - (sizeToReset - this->outpos);
    this->size -= sizeToReset;
    if (freeData)
    {
        this->free += sizeToReset;
    }
}

size_t StreamingBuffer::append(const char *buf, size_t len)
{
    if (!buffer)
    {
        // initialize the buffer if it's not initialized yet
        init(len);
    }

    if (free < len)
    {
        LOG_debug << getLogName()
                  << "[Streaming] Not enough available space, len will be truncated. "
                  << " [requested = " << len << ", buffered = " << free
                  << ", discarded = " << (len - free) << "]";
        len = free;
    }

    // update the internal state
    size_t currentIndex = inpos;
    inpos += len;
    int remaining = static_cast<int>(inpos - capacity);
    inpos %= capacity;
    size += len;
    free -= len;

    // append the new data
    if (remaining <= 0)
    {
        memcpy(buffer + currentIndex, buf, len);
    }
    else
    {
        size_t num = static_cast<size_t>(static_cast<int>(len) - remaining);
        LOG_debug << getLogName()
                  << "[Streaming] Length exceeds limits of circular buffer. Writing a piece of "
                  << num << " bytes to the end and the others " << remaining
                  << " bytes from the beginning"
                  << " [current index = " << currentIndex << ", len = " << len
                  << ", capacity = " << capacity << "]";
        memcpy(buffer + currentIndex, buf, num);
        memcpy(buffer, buf + num, static_cast<size_t>(remaining));
    }

    return len;
}

size_t StreamingBuffer::availableData() const
{
    return size;
}

size_t StreamingBuffer::availableSpace() const
{
    return free;
}

size_t StreamingBuffer::availableCapacity() const
{
    return capacity;
}

uv_buf_t StreamingBuffer::nextBuffer()
{
    if (!size)
    {
        // no data available
        return uv_buf_init(NULL, 0);
    }

    // prepare output buffer
    char *outbuf = buffer + outpos;
    size_t len = size < maxOutputSize ? size : maxOutputSize;
    if (outpos + len > capacity)
    {
        LOG_debug << getLogName()
                  << "[Streaming] Available length exceeds limits of circular buffer: "
                  << "Truncating output buffer length to " << (capacity - outpos) << " bytes"
                  << " [outpos = " << outpos << ", len = " << len << ", capacity = " << capacity
                  << "]";
        len = capacity - outpos;
    }

    // update the internal state
    size -= len;
    outpos += len;
    outpos %= capacity;

    // return the buffer
    return uv_buf_init(outbuf, (unsigned int)(len));
}

void StreamingBuffer::freeData(size_t len)
{
    LOG_verbose << getLogName() << "[Streaming] Streaming buffer free data: len = " << len
                << ", actual free = " << free << ", new free = " << (free + len)
                << ", size = " << size << " [capacity = " << capacity << "]";
    // update the internal state
    free += len;
}

uv_buf_t StreamingBuffer::nextWriteBuffer(size_t maxLen)
{
    if (!buffer || !free)
        return uv_buf_init(nullptr, 0);

    // Contiguous space from inpos to end of backing array (no wrap-around)
    const size_t contiguous = std::min(free, capacity - inpos);
    const size_t len = std::min(contiguous, maxLen);
    return uv_buf_init(buffer + inpos, static_cast<unsigned int>(len));
}

void StreamingBuffer::commitWrite(size_t bytesWritten)
{
    assert(bytesWritten <= free);
    inpos = (inpos + bytesWritten) % capacity;
    size += bytesWritten;
    free -= bytesWritten;
}

void StreamingBuffer::setMaxBufferSize(unsigned int bufferSize)
{
    LOG_debug << getLogName()
              << "[Streaming] Set new max buffer size for StreamingBuffer: " << bufferSize;
    if (bufferSize)
    {
        this->maxBufferSize = bufferSize;
    }
    else
    {
        this->maxBufferSize = MAX_BUFFER_SIZE;
    }
}

void StreamingBuffer::setMaxOutputSize(unsigned int outputSize)
{
    LOG_debug << getLogName()
              << "[Streaming] Set new max output size for StreamingBuffer: " << outputSize;
    if (outputSize)
    {
        this->maxOutputSize = outputSize;
    }
    else
    {
        this->maxOutputSize = MAX_OUTPUT_SIZE;
    }
}

void StreamingBuffer::setFileSize(m_off_t newFileSize)
{
    fileSize = newFileSize;
    LOG_debug << getLogName() << "[Streaming] File size set to " << fileSize << " bytes";
}

void StreamingBuffer::setDuration(int newDuration)
{
    if (!newDuration)
    {
        // Param 'newDuration' is documented in MegaNode::getDuration() to be -1 if it's not
        // defined. Hence, if it's 0, the file should be a media file... which has a duration of 0
        // seconds.
        LOG_warn << getLogName() << "[Streaming] Duration value is 0 seconds for this media file!";
    }
    duration = newDuration > 0 ? newDuration : 0;
    LOG_debug << getLogName() << "[Streaming] File duration set to " << duration << " secs";
}

m_off_t StreamingBuffer::getBytesPerSecond() const
{
    if (fileSize < duration)
    {
        LOG_err << getLogName() << "[Streaming] File size is smaller than its duration in seconds!"
                << " [file size = " << fileSize << " bytes"
                << " , duration = " << duration << " secs]";
    }
    return duration ? (fileSize / duration) : 0;
}

m_off_t StreamingBuffer::partialDuration(m_off_t partialSize) const
{
    partialSize = std::min(partialSize, fileSize);
    m_off_t bytesPerSecond = getBytesPerSecond();
    return bytesPerSecond ? (partialSize / bytesPerSecond) : 0;
}

unsigned StreamingBuffer::getMaxBufferSize()
{
    return this->maxBufferSize ?
                static_cast<unsigned>(this->maxBufferSize) :
                StreamingBuffer::MAX_BUFFER_SIZE;
}

unsigned StreamingBuffer::getMaxOutputSize()
{
    return this->maxOutputSize ?
                static_cast<unsigned>(this->maxOutputSize) :
                StreamingBuffer::MAX_OUTPUT_SIZE;
}

std::string StreamingBuffer::bufferStatus() const
{
    std::string bufferState;
    bufferState.reserve(256);
    bufferState.append("[|Buffer status| buffered = ")
               .append(std::to_string(size));
    if (duration)
        bufferState.append(" (").append(
            std::to_string(partialDuration(static_cast<m_off_t>(size))).append(" secs)"));
    bufferState.append(", free = ").append(std::to_string(free));
    if (duration)
        bufferState.append(" (").append(
            std::to_string(partialDuration(static_cast<m_off_t>(free))).append(" secs)"));
    bufferState.append(", capacity = ").append(std::to_string(capacity));
    if (duration)
        bufferState.append(" (").append(
            std::to_string(partialDuration(static_cast<m_off_t>(capacity))).append(" secs)"));
    bufferState.append("]");
    return bufferState;
}

HttpStreamingBuffer::HttpStreamingBuffer(const std::string& logName):
    StreamingBuffer(logName)
{}

void HttpStreamingBuffer::calcMaxBufferAndMaxOutputSize()
{
    StreamingBuffer::calcMaxBufferAndMaxOutputSize();
    maxBufferSize =
        std::min(maxBufferSize, static_cast<size_t>(HttpStreamingBuffer::HTTP_MAX_BUFFER_SIZE));
    maxOutputSize =
        std::min(maxOutputSize, static_cast<size_t>(HttpStreamingBuffer::HTTP_MAX_OUTPUT_SIZE));
}

// http_parser settings
http_parser_settings MegaTCPServer::parsercfg;

MegaTCPServer::MegaTCPServer(MegaApiImpl* megaApi,
                             string basePath,
                             [[maybe_unused]] bool tls,
                             [[maybe_unused]] string certificatepath,
                             [[maybe_unused]] string keypath,
                             bool ipv6):
    useIPv6(ipv6),
#ifdef ENABLE_EVT_TLS
    useTLS(tls)
#else
    useTLS(false)
#endif
{
    this->megaApi = megaApi;
    this->localOnly = true;
    this->started = false;
    this->port = 0;
    this->maxBufferSize = 0;
    this->maxOutputSize = 0;
    this->restrictedMode = MegaApi::TCP_SERVER_ALLOW_CREATED_LOCAL_LINKS;
    this->lastHandle = INVALID_HANDLE;
    this->remainingcloseevents = 0;
    this->closing = false;
    this->thread = new MegaThread();
#ifdef ENABLE_EVT_TLS
    this->certificatepath = certificatepath;
    this->keypath = keypath;
    this->closing = false;
    this->remainingcloseevents = 0;
    this->evtrequirescleaning = false;
#endif
    fsAccess = mega::createFSA();

    if (basePath.size())
    {
        LocalPath lp = LocalPath::fromAbsolutePath(basePath);
        if (!lp.endsInSeparator())
        {
            lp.appendWithSeparator(LocalPath(), true);
        }
        string sBasePath = lp.toPath(false);
        this->basePath = sBasePath;
    }
    semaphoresdestroyed = false;
    uv_sem_init(&semaphoreEnd, 0);
    uv_sem_init(&semaphoreStartup, 0);
}

MegaTCPServer::~MegaTCPServer()
{
    LOG_verbose << "MegaTCPServer::~MegaTCPServer BEGIN";
    stop();

    thread->join();
    delete thread;

    semaphoresdestroyed = true;
    uv_sem_destroy(&semaphoreStartup);
    uv_sem_destroy(&semaphoreEnd);
    fsAccess.reset();
    LOG_verbose << "MegaTCPServer::~MegaTCPServer END";
}

bool MegaTCPServer::start(int newPort, bool newLocalOnly)
{
    if (started && port == newPort && localOnly == newLocalOnly)
    {
        LOG_verbose << "MegaTCPServer::start Alread started at that port, returning " << started;
        return true;
    }
    if (started)
    {
        stop();
    }

    port = newPort;
    localOnly = newLocalOnly;

    thread->start(threadEntryPoint, this);
    uv_sem_wait(&semaphoreStartup);
    if (!started)
        port = 0;

    LOG_verbose << "MegaTCPServer::start. port = " << port << ", returning " << started;
    return started;
}

#ifdef ENABLE_EVT_TLS
int MegaTCPServer::uv_tls_writer(evt_tls_t *evt_tls, void *bfr, int sz)
{
    int rv = 0;
    uv_buf_t b;
    b.base = (char*)bfr;
    b.len = sz;

    MegaTCPContext *tcpctx = (MegaTCPContext*)evt_tls->data;
    assert(tcpctx != NULL);

    if (uv_is_writable((uv_stream_t*)(&tcpctx->tcphandle)))
    {
        uv_write_t *req = new uv_write_t();
        tcpctx->writePointers.push_back((char*)bfr);
        req->data = tcpctx;
        LOG_verbose << "Sending " << sz << " bytes of TLS data on port = " << tcpctx->server->port;
        if (int err = uv_write(req, (uv_stream_t*)&tcpctx->tcphandle, &b, 1, onWriteFinished_tls_async))
        {
            LOG_warn << "At uv_tls_writer: Finishing due to an error sending the response: " << err;
            tcpctx->writePointers.pop_back();
            delete [] (char*)bfr;
            delete req;

            closeTCPConnection(tcpctx);
        }
        rv = sz; //writer should return the written size
    }
    else
    {
        delete [] (char*)bfr;
        LOG_debug << " uv_is_writable returned false";
    }

    return rv;
}
#endif

// return 0 on error, otherwise a port number (positive)
static int getBoundPort(uv_tcp_t* server)
{
    assert(server);

    struct sockaddr_storage bound_addr;
    int addr_len = sizeof(bound_addr);
    if (uv_tcp_getsockname(server, reinterpret_cast<struct sockaddr*>(&bound_addr), &addr_len))
    {
        // error
        return 0;
    }

    int port = 0;
    if (bound_addr.ss_family == AF_INET)
    {
        const auto addrin = reinterpret_cast<struct sockaddr_in*>(&bound_addr);
        port = ntohs(addrin->sin_port);
    }
    else if (bound_addr.ss_family == AF_INET6)
    {
        const auto addrin6 = reinterpret_cast<struct sockaddr_in6*>(&bound_addr);
        port = ntohs(addrin6->sin6_port);
    }
    else
    {
        // Doesn't support others
        assert(false);
    }

    return port;
}

void MegaTCPServer::run()
{
    LOG_debug << " Running tcp server: " << port << " TLS=" << useTLS;

#ifdef ENABLE_EVT_TLS
    if (useTLS)
    {
        if (evt_ctx_init_ex(&evtctx, certificatepath.c_str(), keypath.c_str()) != 1 )
        {
            LOG_err << "Unable to init evt ctx";
            uv_sem_post(&semaphoreStartup);
            uv_sem_post(&semaphoreEnd);
            return;
        }
        evt_ctx_set_nio(&evtctx, NULL, uv_tls_writer);
    }
#endif

    uv_loop_init(&uv_loop);

    uv_async_init(&uv_loop, &exit_handle, onCloseRequested);
    exit_handle.data = this;

    uv_tcp_init(&uv_loop, &server);
    server.data = this;

    uv_tcp_keepalive(&server, 0, 0);

    union {
        struct sockaddr_in6 ipv6;
        struct sockaddr_in ipv4;
    } address;

    if (useIPv6)
    {
        if (localOnly)
        {
            uv_ip6_addr("::1", port, &address.ipv6);
        }
        else
        {
            uv_ip6_addr("::", port, &address.ipv6);
        }
    }
    else
    {
        if (localOnly)
        {
            uv_ip4_addr("127.0.0.1", port, &address.ipv4);
        }
        else
        {
            uv_ip4_addr("0.0.0.0", port, &address.ipv4);
        }
    }

    uv_connection_cb onNewClientCB;
#ifdef ENABLE_EVT_TLS
    if (useTLS)
    {
         onNewClientCB = onNewClient_tls;
    }
    else
    {
#endif
        onNewClientCB = onNewClient;
#ifdef ENABLE_EVT_TLS
    }
#endif

    auto cleanOnError = [this](const char* action)
    {
        LOG_err << "TCP failed to " << action << " = " << port;

        uv_close((uv_handle_t *)&exit_handle,NULL);
        uv_close((uv_handle_t *)&server,NULL);
        uv_sem_post(&semaphoreStartup);
        uv_sem_post(&semaphoreEnd);
        uv_run(&uv_loop, UV_RUN_ONCE); // so that resources are cleaned peacefully
        int closeVal = uv_loop_close(&uv_loop); // Clean up loop resources
        if (closeVal)
        {
            LOG_err << "[MegaTCPServer::run] Error closing uv_loop: " << uv_strerror(closeVal);
        }
    };

    if (uv_tcp_bind(&server, (const struct sockaddr*)&address, 0))
    {
        cleanOnError("bind port");
        return;
    }

    // Update port to bound port if request port is 0
    if (port == 0)
    {
        port = getBoundPort(&server);
        if (port == 0) // still zero?
        {
            cleanOnError("get bound port");
            return;
        }
    }

    if (uv_listen((uv_stream_t*)&server, 32, onNewClientCB))
    {
        cleanOnError("listen port");
        return;
    }

    LOG_info << "TCP" << (useTLS ? "(tls)" : "") << " server started on port " << port;
    started = true;
    uv_sem_post(&semaphoreStartup);

    LOG_info << "Starting uv loop ...";
    uv_run(&uv_loop, UV_RUN_DEFAULT);

    // Can get here only after stop() has been called
    LOG_info << "UV loop ended";
#ifdef ENABLE_EVT_TLS
    if (useTLS)
    {
        //evt_ctx_free(&evtctx); //This causes invalid free when called second time!! collides with memory allocated elsewhere (e.g: via curl_global_init!)
        SSL_CTX_free(evtctx.ctx);
    }
#endif
    int closeVal = uv_loop_close(&uv_loop); // Clean up loop resources
    if (closeVal)
    {
        LOG_err << "[MegaTCPServer::run] Error closing uv_loop: " << uv_strerror(closeVal);
    }
    LOG_debug << "UV loop thread exit";
}

void MegaTCPServer::stop(bool doNotWait)
{
    if (!started)
    {
        LOG_verbose << "Stopping non started MegaTCPServer port=" << port;
        return;
    }

    LOG_debug << "Stopping MegaTCPServer port = " << port;
    uv_async_send(&exit_handle);
    if (!doNotWait)
    {
        LOG_verbose << "Waiting for sempahoreEnd to conclude server stop port = " << port;
        uv_sem_wait(&semaphoreEnd); //this is signaled when closed my last connection
    }
    LOG_debug << "Stopped MegaTCPServer port = " << port;
    started = false;
    port = 0;
}

int MegaTCPServer::getPort()
{
    return port;
}

uv_loop_t* MegaTCPServer::getUvLoop()
{
    return &uv_loop;
}

bool MegaTCPServer::isLocalOnly()
{
    return localOnly;
}

void MegaTCPServer::setMaxBufferSize(int bufferSize)
{
    this->maxBufferSize = bufferSize <= 0 ? 0 : bufferSize;
}

void MegaTCPServer::setMaxOutputSize(int outputSize)
{
    this->maxOutputSize = outputSize <= 0 ? 0 : outputSize;
}

int MegaTCPServer::getMaxBufferSize()
{
    if (maxBufferSize)
    {
        return maxBufferSize;
    }

    return StreamingBuffer::MAX_BUFFER_SIZE;
}

int MegaTCPServer::getMaxOutputSize()
{
    if (maxOutputSize)
    {
        return maxOutputSize;
    }

    return StreamingBuffer::MAX_OUTPUT_SIZE;
}

void MegaTCPServer::setRestrictedMode(int mode)
{
    this->restrictedMode = mode;
}

int MegaTCPServer::getRestrictedMode()
{
    return restrictedMode;
}

bool MegaTCPServer::isHandleAllowed(handle h)
{
    return restrictedMode == MegaApi::TCP_SERVER_ALLOW_ALL
            || (restrictedMode == MegaApi::TCP_SERVER_ALLOW_CREATED_LOCAL_LINKS && allowedHandles.count(h))
            || (restrictedMode == MegaApi::TCP_SERVER_ALLOW_LAST_LOCAL_LINK && h == lastHandle);
}

void MegaTCPServer::clearAllowedHandles()
{
    allowedHandles.clear();
    lastHandle = INVALID_HANDLE;
}

char *MegaTCPServer::getLink(MegaNode *node, string protocol)
{
    if (!node)
    {
        return NULL;
    }

    lastHandle = node->getHandle();
    allowedHandles.insert(lastHandle);

    string localhostIP = useIPv6 ? "[::1]" : "127.0.0.1";

    ostringstream oss;
    oss << protocol << (useTLS ? "s" : "") << "://" << localhostIP << ":" << port << "/";
    char *base64handle = node->getBase64Handle();
    oss << base64handle;
    delete [] base64handle;

    if (node->isPublic() || node->isForeign())
    {
        char *base64key = node->getBase64Key();
        oss << "!" << base64key;
        delete [] base64key;

        if (node->isForeign())
        {
            oss << "!" << node->getSize();
            auto nodePrivate{dynamic_cast<MegaNodePrivate*>(node)};
            if (!nodePrivate)
            {
                LOG_err << "Critical error: Unexpected MegaNode extension received";
                assert(false);
                return nullptr;
            }
            string* publicAuth = nodePrivate->getPublicAuth();
            string* privAuth = nodePrivate->getPrivateAuth();
            const char* chatAuth = nodePrivate->getChatAuth();
            if (privAuth->size())
            {
                oss << "!f" << *privAuth;
            }
            else if (publicAuth->size())
            {
                oss << "!p" << *publicAuth;
            }
            else if (chatAuth && chatAuth[0])
            {
                oss << "!c" << chatAuth;
            }
        }
    }

    oss << "/";

    string name = node->getName();
    string escapedName;
    URLCodec::escape(&name, &escapedName);
    oss << escapedName;
    string link = oss.str();
    return MegaApi::strdup(link.c_str());
}

set<handle> MegaTCPServer::getAllowedHandles()
{
    return allowedHandles;
}

void MegaTCPServer::removeAllowedHandle(MegaHandle handle)
{
    allowedHandles.erase(handle);
}

void *MegaTCPServer::threadEntryPoint(void *param)
{
#ifndef _WIN32
    struct sigaction noaction;
    memset(&noaction, 0, sizeof(noaction));
    noaction.sa_handler = SIG_IGN;
    ::sigaction(SIGPIPE, &noaction, 0);
#endif

    MegaTCPServer *tcpServer = (MegaTCPServer *)param;
    tcpServer->run();
    return NULL;
}

#ifdef ENABLE_EVT_TLS
void MegaTCPServer::evt_on_rd(evt_tls_t *evt_tls, char *bfr, int sz)
{
    MegaTCPContext *tcpctx = (MegaTCPContext*)evt_tls->data;
    assert(tcpctx != NULL);

    uv_buf_t data;
    data.base = bfr;
    data.len = sz;

    if (!tcpctx->invalid)
    {
        tcpctx->server->processReceivedData(tcpctx, sz, &data);
    }
    else
    {
        LOG_debug << " Not procesing invalid data after failed evt_close";
    }
}

void MegaTCPServer::on_evt_tls_close(evt_tls_t *evt_tls, int status)
{
    MegaTCPContext *tcpctx = (MegaTCPContext*)evt_tls->data;
    assert(tcpctx != NULL);

    LOG_debug << "TLS connection closed. status = " << status;

    if (status == 1)
    {
        closeTCPConnection(tcpctx);
    }
    else
    {
        LOG_debug << "TLS connection closed failed!!! status = " << status;
        tcpctx->invalid = true;
    }
}

void MegaTCPServer::on_hd_complete( evt_tls_t *evt_tls, int status)
{
    MegaTCPContext *tcpctx = (MegaTCPContext*)evt_tls->data;
    LOG_debug << "TLS handshake finished in port = " << tcpctx->server->port << ". Status: " << status;

    if (status)
    {
        evt_tls_read(evt_tls, evt_on_rd); //this only establish callback
        if ( tcpctx->server->respondNewConnection(tcpctx) )
        {
            // we dont need to explicitally start reading. on_tcp_read will be called
        }
    }
    else
    {
        evt_tls_close(evt_tls, on_evt_tls_close);
    }
}

void MegaTCPServer::onNewClient_tls(uv_stream_t *server_handle, int status)
{
    if (status < 0)
    {
        LOG_warn << " onNewClient_tls unexpected status: " << status;
        return;
    }

    // Create an object to save context information
    auto tcpctx = ((MegaTCPServer*)server_handle->data)->initializeContext(server_handle);

    LOG_debug << "Connection received at port " << tcpctx->server->port << " ! " << tcpctx->server->connections.size();

    // Async handle to perform writes
    uv_async_init(&tcpctx->server->uv_loop, &tcpctx->asynchandle, onAsyncEvent);

    // Accept the connection
    uv_tcp_init(&tcpctx->server->uv_loop, &tcpctx->tcphandle);
    if (uv_accept(server_handle, (uv_stream_t*)&tcpctx->tcphandle))
    {
        LOG_err << "uv_accept failed";
        onClose((uv_handle_t*)&tcpctx->tcphandle);
        return;
    }

    tcpctx->evt_tls = evt_ctx_get_tls(&tcpctx->server->evtctx);
    assert(tcpctx->evt_tls != NULL);
    tcpctx->evt_tls->data = tcpctx.get();
    if (evt_tls_accept(tcpctx->evt_tls, on_hd_complete))
    {
        LOG_err << "evt_tls_accept failed";
        evt_tls_close(tcpctx->evt_tls, on_evt_tls_close);
        return;
    }

    tcpctx->server->connections.add(tcpctx);

    tcpctx->server->readData(tcpctx.get());
}
#endif

void MegaTCPServer::readData(MegaTCPContext* tcpctx)
{
#ifdef ENABLE_EVT_TLS
    if (useTLS)
    {
        uv_read_start((uv_stream_t*)(&tcpctx->tcphandle), allocBuffer, on_tcp_read);
    }
    else
    {
#endif
        uv_read_start((uv_stream_t*)&tcpctx->tcphandle, allocBuffer, onDataReceived);
#ifdef ENABLE_EVT_TLS
    }
#endif
}

void MegaTCPServer::onNewClient(uv_stream_t* server_handle, int status)
{
    if (status < 0)
    {
        return;
    }

    // Create an object to save context information
    auto tcpctx = ((MegaTCPServer*)server_handle->data)->initializeContext(server_handle);

    LOG_debug << "Connection received at port " << tcpctx->server->port << "! "
              << tcpctx->server->connections.size() << " tcpctx = " << tcpctx.get();

    // Async handle to perform writes
    uv_async_init(&tcpctx->server->uv_loop, &tcpctx->asynchandle, onAsyncEvent);

    // Accept the connection
    uv_tcp_init(&tcpctx->server->uv_loop, &tcpctx->tcphandle);
    if (uv_accept(server_handle, (uv_stream_t*)&tcpctx->tcphandle))
    {
        LOG_err << "uv_accept failed";
        onClose((uv_handle_t*)&tcpctx->tcphandle);
        return;
    }

    if (!tcpctx->server->connections.add(tcpctx))
    {
        assert(false); // Shouldn't happen. It is usually a bug.
        LOG_err << "Adding tcpctx to connections failed";
        onClose((uv_handle_t*)&tcpctx->tcphandle);
        return;
    }

    if (tcpctx->server->respondNewConnection(tcpctx.get()))
    {
        // Start reading
        tcpctx->server->readData(tcpctx.get());
    }
}

void MegaTCPServer::allocBuffer(uv_handle_t *, size_t suggested_size, uv_buf_t* buf)
{
    // Reserve a buffer with the suggested size
    *buf = uv_buf_init(new char[suggested_size], static_cast<unsigned>(suggested_size));
}

void MegaTCPServer::onDataReceived(uv_stream_t* tcp, ssize_t nread, const uv_buf_t * buf)
{
    MegaTCPContext *tcpctx = (MegaTCPContext*) tcp->data;
    tcpctx->server->processReceivedData(tcpctx, nread, buf);
    delete [] buf->base;
}

#ifdef ENABLE_EVT_TLS
void MegaTCPServer::on_tcp_read(uv_stream_t *tcp, ssize_t nrd, const uv_buf_t *data)
{
    MegaTCPContext *tcpctx = (MegaTCPContext*) tcp->data;
    assert( tcpctx != NULL);

    LOG_debug << "Received " << nrd << " bytes at port " << tcpctx->server->port;
    if (!nrd)
    {
        return;
    }

    if (nrd < 0)
    {
        if (evt_tls_is_handshake_over(tcpctx->evt_tls))
        {
            LOG_verbose << "MegaTCPServer::on_tcp_read calling processReceivedData";
            tcpctx->server->processReceivedData(tcpctx, nrd, data);
            evt_tls_close(tcpctx->evt_tls, on_evt_tls_close);
        }
        else
        {
            //if handshake is not over, simply tear down without close_notify
            closeTCPConnection(tcpctx);
        }
        delete[] data->base;
        return;
    }

    evt_tls_feed_data(tcpctx->evt_tls, data->base, static_cast<int>(nrd));
    delete[] data->base;
}
#endif

void MegaTCPServer::onClose(uv_handle_t* handle)
{
    MegaTCPContext* tcpctx = (MegaTCPContext*) handle->data;

    // streaming transfers are automatically stopped when their listener is removed
    tcpctx->megaApi->removeTransferListener(tcpctx);
    tcpctx->megaApi->removeRequestListener(tcpctx);

    // Move from connections to closingConnections to keep tcpctx alive until onAsyncEventClose
    if (auto tcpCtxPtr = tcpctx->server->connections.release(tcpctx); tcpCtxPtr)
    {
        tcpctx->server->closingConnections[tcpctx] = std::move(tcpCtxPtr);
    }

    LOG_debug << "Connection closed: " << tcpctx->server->connections.size() << " port = " << tcpctx->server->port << " closing async handle";
    uv_close((uv_handle_t *)&tcpctx->asynchandle, onAsyncEventClose);
}

void MegaTCPServer::onAsyncEventClose(uv_handle_t *handle)
{
    MegaTCPContext* tcpctx = (MegaTCPContext*) handle->data;
    assert(!tcpctx->writePointers.size());

    int port = tcpctx->server->port;

    tcpctx->server->remainingcloseevents--;
    tcpctx->server->processOnAsyncEventClose(tcpctx);

    LOG_verbose << "At onAsyncEventClose port = " << tcpctx->server->port << " remaining=" << tcpctx->server->remainingcloseevents;

    if (!tcpctx->server->remainingcloseevents && tcpctx->server->closing && !tcpctx->server->semaphoresdestroyed)
    {
        uv_sem_post(&tcpctx->server->semaphoreStartup);
        uv_sem_post(&tcpctx->server->semaphoreEnd);
    }

    // Now it's safe to release the shared_ptr - this will destroy tcpctx if it's the last reference
    tcpctx->server->closingConnections.erase(tcpctx);

    LOG_debug << "Connection cleanup completed, port = " << port;
}

#ifdef ENABLE_EVT_TLS
void MegaTCPServer::onWriteFinished_tls(evt_tls_t *evt_tls, int status)
{
    MegaTCPContext *tcpctx = (MegaTCPContext*)evt_tls->data;
    assert(tcpctx != NULL);

    if (status < 0)
    {
        LOG_warn << " error received at onWriteFinished_tls: " << status;
    }

    if (tcpctx->finished)
    {
        LOG_debug << "At onWriteFinished_tls; TCP link closed, ignoring the result of the write";
        return;
    }
    tcpctx->server->processWriteFinished(tcpctx, status);
}

void MegaTCPServer::onWriteFinished_tls_async(uv_write_t* req, int status)
{
    MegaTCPContext *tcpctx = (MegaTCPContext*)req->data;
    assert(tcpctx->writePointers.size());
    delete [] tcpctx->writePointers.front();
    tcpctx->writePointers.pop_front();
    delete req;

    if (tcpctx->finished)
    {
        if (tcpctx->size == tcpctx->bytesWritten && !tcpctx->writePointers.size())
        {
            LOG_debug << "TCP link closed, shutdown result: " << status << " port = " << tcpctx->server->port;
        }
        else
        {
            LOG_debug << "TCP link closed, ignoring the result of the async TLS write: " << status << " port = " << tcpctx->server->port;
        }
        return;
    }

    if (status < 0)
    {
        LOG_warn << "Finishing request. Async TLS write failed: " << status;
        evt_tls_close(tcpctx->evt_tls, on_evt_tls_close);
        return;
    }

    if (tcpctx->size == tcpctx->bytesWritten && !tcpctx->writePointers.size())
    {
        LOG_debug << "Finishing request. All data delivered";
        evt_tls_close(tcpctx->evt_tls, on_evt_tls_close);
        return;
    }

    LOG_verbose << "Async TLS write finished";
    uv_async_send(&tcpctx->asynchandle);
}
#endif

void MegaTCPServer::onWriteFinished(uv_write_t* req, int status)
{
    MegaTCPContext* tcpctx = (MegaTCPContext*) req->data;
    assert(tcpctx != NULL);
    if (tcpctx->finished)
    {
        LOG_debug << "At onWriteFinished; TCP link closed, ignoring the result of the write";
        delete req;
        return;
    }

    tcpctx->server->processWriteFinished(tcpctx, status);
    delete req;
}

MegaTCPContext::MegaTCPContext()
{
    size = -1;
    finished = false;
    bytesWritten = 0;
#ifdef ENABLE_EVT_TLS
    evt_tls = NULL;
    invalid = false;
#endif
    server = NULL;
    megaApi = NULL;

    // Mutex to protect the data buffer
    uv_mutex_init(&mutex);
}

MegaTCPContext::~MegaTCPContext()
{
    uv_mutex_destroy(&mutex);

#ifdef ENABLE_EVT_TLS
    if (evt_tls)
    {
        evt_tls_free(evt_tls);
    }
#endif

    if (!finished && megaApi) // Set listener pointers to NULL upon premature destruction
    {
        megaApi->removeTransferListener(this);
        megaApi->removeRequestListener(this);
    }
}

void MegaTCPServer::onAsyncEvent(uv_async_t* handle)
{
    MegaTCPContext* tcpctx = (MegaTCPContext*) handle->data;

    assert(tcpctx->server != NULL);
#ifdef ENABLE_EVT_TLS
    if (tcpctx->server->useTLS && !evt_tls_is_handshake_over(tcpctx->evt_tls))
    {
        LOG_debug << " skipping processAsyncEvent due to handshake not over on port = " << tcpctx->server->port;
        return;
    }
#endif
    tcpctx->server->processAsyncEvent(tcpctx);
}

void MegaTCPServer::onExitHandleClose(uv_handle_t *handle)
{
    MegaTCPServer *tcpServer = (MegaTCPServer*) handle->data;
    assert(tcpServer != NULL);

    tcpServer->remainingcloseevents--;
    LOG_verbose << "At onExitHandleClose port = " << tcpServer->port << " remainingcloseevent = " << tcpServer->remainingcloseevents;

    tcpServer->processOnExitHandleClose(tcpServer);

    if (!tcpServer->remainingcloseevents && !tcpServer->semaphoresdestroyed)
    {
        uv_sem_post(&tcpServer->semaphoreStartup);
        uv_sem_post(&tcpServer->semaphoreEnd);
    }
}

void MegaTCPServer::onCloseRequested(uv_async_t *handle)
{
    MegaTCPServer *tcpServer = (MegaTCPServer*) handle->data;
    LOG_debug << "TCP server stopping port=" << tcpServer->port;

    tcpServer->closing = true;

    for (auto& tcpctx: tcpServer->connections.copy())
    {
        closeTCPConnection(tcpctx);
    }

    tcpServer->remainingcloseevents++;
    LOG_verbose << "At onCloseRequested: closing server port = " << tcpServer->port << " remainingcloseevent = " << tcpServer->remainingcloseevents;
    uv_close((uv_handle_t *)&tcpServer->server, onExitHandleClose);
    tcpServer->remainingcloseevents++;
    LOG_verbose << "At onCloseRequested: closing exit_handle port = " << tcpServer->port << " remainingcloseevent = " << tcpServer->remainingcloseevents;
    uv_close((uv_handle_t *)&tcpServer->exit_handle, onExitHandleClose);
}

void MegaTCPServer::closeConnection(MegaTCPContext *tcpctx)
{
    LOG_verbose << "At closeConnection port = " << tcpctx->server->port;
#ifdef ENABLE_EVT_TLS
    if (tcpctx->server->useTLS)
    {
        evt_tls_close(tcpctx->evt_tls, on_evt_tls_close);
    }
    else
    {
#endif
        closeTCPConnection(tcpctx);
        return;
#ifdef ENABLE_EVT_TLS
    }
#endif
}

void MegaTCPServer::closeTCPConnection(MegaTCPContext *tcpctx)
{
    tcpctx->finished = true;
    if (!uv_is_closing((uv_handle_t*)&tcpctx->tcphandle))
    {
        tcpctx->server->remainingcloseevents++;
        LOG_verbose << "At closeTCPConnection port = " << tcpctx->server->port << " remainingcloseevent = " << tcpctx->server->remainingcloseevents;
        uv_close((uv_handle_t*)&tcpctx->tcphandle, onClose);
    }
}

void MegaTCPServer::processOnAsyncEventClose(MegaTCPContext*) // without this closing breaks!
{
    LOG_debug << "At supposed to be virtual processOnAsyncEventClose";
}

void MegaTCPServer::processOnExitHandleClose(MegaTCPServer*) // without this closing breaks!
{
    LOG_debug << "At supposed to be virtual processOnExitHandleClose";
}

void MegaTCPServer::processReceivedData(MegaTCPContext*, ssize_t /*nread*/, const uv_buf_t*)
{
    LOG_debug << "At supposed to be virtual processReceivedData";
}

void MegaTCPServer::processAsyncEvent(MegaTCPContext*)
{
    LOG_debug << "At supposed to be virtual processAsyncEvent";
}

///////////////////////////////
//  MegaHTTPServer specifics //
///////////////////////////////

MegaHTTPServer::MegaHTTPServer(MegaApiImpl *megaApi, string basePath, bool useTLS, string certificatepath, string keypath, bool useIPv6)
    : MegaTCPServer(megaApi, basePath, useTLS, certificatepath, keypath, useIPv6)
{
    // parser callbacks
    parsercfg.on_url = onUrlReceived;
    parsercfg.on_message_begin = onMessageBegin;
    parsercfg.on_headers_complete = onHeadersComplete;
    parsercfg.on_message_complete = onMessageComplete;
    parsercfg.on_header_field = onHeaderField;
    parsercfg.on_header_value = onHeaderValue;
    parsercfg.on_body = onBody;

    this->fileServerEnabled = true;
    this->folderServerEnabled = true;
    this->offlineAttribute = false;
    this->subtitlesSupportEnabled = false;
}

MegaTCPContextPtr MegaHTTPServer::initializeContext(uv_stream_t* server_handle)
{
    shared_ptr<MegaHTTPContext> httpctx = std::make_shared<MegaHTTPContext>();

    // Initialize the parser
    http_parser_init(&httpctx->parser, HTTP_REQUEST);

    // Set connection data
    MegaHTTPServer* httpServer = (MegaHTTPServer*)(server_handle->data);

    httpctx->server = httpServer;
    httpctx->megaApi = httpServer->megaApi;
    httpctx->parser.data = httpctx.get();
    httpctx->tcphandle.data = httpctx.get();
    httpctx->asynchandle.data = httpctx.get();

    return httpctx;
}

void MegaHTTPServer::processReceivedData(MegaTCPContext *tcpctx, ssize_t nread, const uv_buf_t * buf)
{
    MegaHTTPContext* httpctx = dynamic_cast<MegaHTTPContext *>(tcpctx);

    LOG_debug << httpctx->getLogName() << "Received " << nread << " bytes";

    ssize_t parsed = -1;
    if (nread >= 0)
    {
        if (nread == 0 && httpctx->parser.method == HTTP_PUT) //otherwise it will fail for files >65k in GVFS-DAV
        {
            LOG_debug << httpctx->getLogName() << " Skipping parsing 0 length data for HTTP_PUT";
            parsed = 0;
        }
        else
        {
            parsed = static_cast<ssize_t>(http_parser_execute(&httpctx->parser,
                                                              &parsercfg,
                                                              buf->base,
                                                              static_cast<size_t>(nread)));
        }
    }

    LOG_verbose << httpctx->getLogName() << " at onDataReceived, received " << nread
                << " parsed = " << parsed;

    if (parsed < 0 || nread < 0 || parsed < nread || httpctx->parser.upgrade)
    {
        LOG_debug << httpctx->getLogName()
                  << "Finishing request. Connection reset by peer or unsupported data";
        closeConnection(httpctx);
    }
}

using file_service::FileID;
using file_service::FileResultOr;
using file_service::FileStreamFDCallback;
using file_service::FileStreamFDResult;

/*
 * Context structure to carry the request and its associated buffer
 * throughout the asynchronous lifecycle.
 */
struct ReadContext
{
    static constexpr unsigned int MAX_READ_SIZE = 2 * 1024 * 1024;
    uv_fs_t read_req{};
    uv_buf_t iov{};
    std::weak_ptr<MegaHTTPContext> ctx;
};

static void onReadComplete(uv_fs_t* req)
{
    assert(req);

    // Take ownership
    std::unique_ptr<ReadContext> ctx{(ReadContext*)req->data};

    // Cleanup while leaving the function
    const auto reqDestructor = ScopedDestructor(
        [req]()
        {
            uv_fs_req_cleanup(req);
        });

    auto httpctx = ctx->ctx.lock();
    if (!httpctx)
        return;

    httpctx->mCacheFile.setReading(false);

    if (httpctx->finished)
        return;

    if (const auto readSize = req->result; readSize < 0)
    {
        httpctx->failed = true;
        LOG_err << "Read error: " << uv_strerror((int)req->result);
    }
    else if (readSize == 0)
    {
        // It may happen if cache file is modified
        httpctx->failed = true;
        LOG_err << "Unexpected EOF in cache file";
    }
    else if (readSize > 0)
    {
        assert(ctx->iov.len >= static_cast<size_t>(readSize));

        uv_mutex_lock(&httpctx->mutex);
        httpctx->streamingBuffer.commitWrite(static_cast<size_t>(readSize));
        uv_mutex_unlock(&httpctx->mutex);

        httpctx->mCacheFile.addConsumedBytes(readSize);
    }

    // Read complete and/or more data is in streaming buffer
    uv_async_send(&httpctx->asynchandle);
}

static void readCacheFile(MegaHTTPContext* httpctx)
{
    const auto availableBytes = httpctx->mCacheFile.availableBytes();
    const auto consumedBytes = httpctx->mCacheFile.consumedBytes();
    const auto fd = httpctx->mCacheFile.fd();
    const auto offset = httpctx->mCacheFile.offset();

    // Cannot read more from fd to stream buffer
    if (fd < 0 || availableBytes <= consumedBytes)
        return;

    uv_mutex_lock(&httpctx->mutex);
    const auto iov = httpctx->streamingBuffer.nextWriteBuffer(ReadContext::MAX_READ_SIZE);
    uv_mutex_unlock(&httpctx->mutex);

    // streamingBuffer is full
    if (!iov.base || !iov.len)
        return;

    // Another is reading
    if (httpctx->mCacheFile.isReading())
    {
        LOG_verbose << httpctx->getLogName() << "[Streaming] Skip reading, another is reading";
        return;
    }

    const auto readLength = std::min(static_cast<unsigned int>(iov.len),
                                     static_cast<unsigned int>(availableBytes - consumedBytes));
    const auto readOffset = offset + consumedBytes;

    HTTP_verbose_timed << httpctx->getLogName() << "[Streaming] Read more from file: " << readOffset
                       << ", " << readLength;

    auto ctx = std::make_unique<ReadContext>();
    ctx->read_req.data = ctx.get();
    ctx->ctx = httpctx->weak_from_this();
    ctx->iov = uv_buf_init(iov.base, readLength);

    const int r = uv_fs_read(httpctx->server->getUvLoop(), /* Loop */
                             &ctx->read_req, /* Request object */
                             fd, /* File descriptor */
                             &ctx->iov, /* Pointer to an array of buffers */
                             1, /* Number of buffers in the array */
                             readOffset, /* Offset in the file */
                             onReadComplete);
    if (r < 0)
    {
        LOG_err << httpctx->getLogName()
                << "[Streaming] Failed to initiate read: " << uv_strerror(r);
        httpctx->failed = true;
        uv_fs_req_cleanup(&ctx->read_req);
    }
    else
    {
        httpctx->mCacheFile.setReading(true);
        // Ownership belongs to the onReadComplete
        std::ignore = ctx.release();
    }
}

static AutoUVFile dupUVFile(file_service::File& file)
{
    auto fd = file.dupFileDescriptor();
    if (!fd)
    {
        LOG_err << "Failed to dup fd " << toNodeHandle(file.info().handle())
                << " for streaming errno: " << errno;
        return AutoUVFile{};
    }

    AutoUVFile uvFile = uv_open_osfhandle(fd.get());
    if (!uvFile)
    {
        LOG_err << "Failed to uv_open_osfhandle fd " << toNodeHandle(file.info().handle())
                << " for streaming";
    }
    else
    {
        fd.release();
    }

    return uvFile;
}

void MegaHTTPServer::processWriteFinished(MegaTCPContext* tcpctx, int status)
{
    MegaHTTPContext* httpctx = dynamic_cast<MegaHTTPContext *>(tcpctx);
    if (httpctx->finished)
    {
        LOG_debug << httpctx->getLogName() << "HTTP link closed, ignoring the result of the write";
        return;
    }

    httpctx->bytesWritten += httpctx->lastBufferLen;
    LOG_verbose << httpctx->getLogName() << "Bytes written: " << httpctx->lastBufferLen
                << " Remaining: " << (httpctx->size - httpctx->bytesWritten);
    httpctx->lastBuffer = NULL;

    assert(httpctx->size >= httpctx->bytesWritten);
    if (status < 0 || httpctx->size == httpctx->bytesWritten)
    {
        if (status < 0)
        {
            LOG_warn << httpctx->getLogName() << "Finishing request. Write failed: " << status;
        }
        else
        {
            LOG_debug << httpctx->getLogName() << "Finishing request. All data sent";
            if (httpctx->resultCode == API_EINTERNAL)
            {
                httpctx->resultCode = API_OK;
            }
        }

        closeConnection(httpctx);
        return;
    }

    uv_mutex_lock(&httpctx->mutex);
    if (httpctx->lastBufferLen)
    {
        httpctx->streamingBuffer.freeData(httpctx->lastBufferLen);
        httpctx->lastBufferLen = 0;
    }
    uv_mutex_unlock(&httpctx->mutex);

    // Read more from fd to stream buffer if any
    readCacheFile(httpctx);

    uv_async_send(&httpctx->asynchandle);
}

void MegaHTTPServer::processOnAsyncEventClose(MegaTCPContext* tcpctx)
{
    MegaHTTPContext* httpctx = dynamic_cast<MegaHTTPContext *>(tcpctx);

    if (httpctx->resultCode == API_EINTERNAL)
    {
        httpctx->resultCode = API_EINCOMPLETE;
    }

    delete httpctx->node;
    httpctx->node = NULL;
}

bool MegaHTTPServer::respondNewConnection(MegaTCPContext*)
{
    return true;
}

void MegaHTTPServer::processOnExitHandleClose(MegaTCPServer*) {}

MegaHTTPServer::~MegaHTTPServer()
{
    // if not stopped, the uv thread might want to access a pointer to this.
    // though this is done in the parent destructor, it could try to access it after vtable has been erased
    stop();
}

bool MegaHTTPServer::isHandleWebDavAllowed(handle h)
{
    return allowedWebDavHandles.count(h) > 0;
}

void MegaHTTPServer::clearAllowedHandles()
{
    allowedWebDavHandles.clear();
    MegaTCPServer::clearAllowedHandles();
}

set<handle> MegaHTTPServer::getAllowedWebDavHandles()
{
    return allowedWebDavHandles;
}

void MegaHTTPServer::removeAllowedWebDavHandle(MegaHandle handle)
{
    allowedWebDavHandles.erase(handle);
}

void MegaHTTPServer::enableFileServer(bool enable)
{
    this->fileServerEnabled = enable;
}

void MegaHTTPServer::enableFolderServer(bool enable)
{
    this->folderServerEnabled = enable;
}

void MegaHTTPServer::enableOfflineAttribute(bool enable)
{
    this->offlineAttribute = enable;
}

bool MegaHTTPServer::isFileServerEnabled()
{
    return fileServerEnabled;
}

bool MegaHTTPServer::isFolderServerEnabled()
{
    return folderServerEnabled;
}

bool MegaHTTPServer::isOfflineAttributeEnabled()
{
    return offlineAttribute;
}

bool MegaHTTPServer::isSubtitlesSupportEnabled()
{
    return subtitlesSupportEnabled;
}

void MegaHTTPServer::enableSubtitlesSupport(bool enable)
{
    this->subtitlesSupportEnabled = enable;
}

char *MegaHTTPServer::getWebDavLink(MegaNode *node)
{
    allowedWebDavHandles.insert(node->getHandle());
    return getLink(node);
}

int MegaHTTPServer::onMessageBegin(http_parser *)
{
    return 0;
}

int MegaHTTPServer::onHeadersComplete(http_parser *)
{
    return 0;
}

int MegaHTTPServer::onUrlReceived(http_parser *parser, const char *url, size_t length)
{
    MegaHTTPContext* httpctx = (MegaHTTPContext*) parser->data;
    httpctx->path.assign(url, length);
    LOG_debug << httpctx->getLogName() << "URL received: " << httpctx->path;

    if (length < 9 || url[0] != '/' || (length >= 10 && url[9] != '/' && url[9] != '!'))
    {
        LOG_debug << httpctx->getLogName() << "URL without node handle";
        return 0;
    }

    size_t index = 9;
    httpctx->nodehandle.assign(url + 1, 8);
    LOG_debug << httpctx->getLogName() << "Node handle: " << httpctx->nodehandle;

    if (length > 53 && url[index] == '!')
    {
        httpctx->nodekey.assign(url + 10, 43);
        LOG_debug << httpctx->getLogName() << "Link key: " << httpctx->nodekey;
        index = 53;

        if (length > 54 && url[index] == '!')
        {
           const char* startsize = url + index + 1;
           const char* endsize = strstr(startsize, "/");
           const char* endparam = strstr(startsize, "!");
           if (endsize && *startsize >= '0' && *startsize <= '9')
           {
               char* endptr;
               m_off_t size = strtoll(startsize, &endptr, 10);
               if ((endptr == endsize || endptr == endparam) && errno != ERANGE)
               {
                   httpctx->nodesize = size;
                   LOG_debug << httpctx->getLogName() << "Link size: " << size;
                   index += static_cast<size_t>(endptr - startsize) + 1u;
                   if (url[index] == '!')
                   {
                       const char *typeauth = url + index + 1;
                       index++;

                       const char *ptr = url + index + 1;
                       string auth;
                       auth.assign(ptr, static_cast<size_t>(endsize - ptr));
                       if (*typeauth == 'p')
                       {
                           assert(auth.size() == 8);
                           httpctx->nodepubauth = auth;
                           LOG_debug << httpctx->getLogName() << "Link public auth: " << auth;
                       }
                       else if (*typeauth == 'c')
                       {
                           assert(auth.size() == 8);
                           httpctx->nodechatauth = auth;
                           LOG_debug << httpctx->getLogName() << "Chat link auth: " << auth;
                       }
                       else if (*typeauth == 'f')
                       {
                           httpctx->nodeprivauth = auth;
                           LOG_debug << httpctx->getLogName() << "Link private auth: " << auth;
                       }
                       else
                       {
                           LOG_err << httpctx->getLogName()
                                   << "Unknown type of auth token: " << *typeauth;
                       }
                       index += auth.size() + 1;
                   }
               }
           }
        }
    }

    if (length > index && url[index] != '/')
    {
        LOG_warn << httpctx->getLogName() << "Invalid URL";
        return 0;
    }

    index++;
    if (length > index)
    {
        string nodename(url + index, length - index);

        //get subpath (used in webdav)
        size_t psep = nodename.find("/");
        if (psep != string::npos)
        {
            string subpathrelative = nodename.substr(psep + 1);
            nodename = nodename.substr(0, psep);
            URLCodec::unescape(&subpathrelative, &httpctx->subpathrelative);
            LOG_debug << httpctx->getLogName() << "subpathrelative: " << httpctx->subpathrelative;
        }

        URLCodec::unescape(&nodename, &httpctx->nodename);
        LocalPath::utf8_normalize(&httpctx->nodename);
        LOG_debug << httpctx->getLogName() << "Node name: " << httpctx->nodename;
    }

    return 0;
}

int MegaHTTPServer::onHeaderField(http_parser *parser, const char *at, size_t length)
{
    MegaHTTPContext* httpctx = (MegaHTTPContext*) parser->data;
    httpctx->lastheader = string(at, length);
    tolower_string(httpctx->lastheader);

    if (Utils::startswith(at, "Range"))
    {
        httpctx->range = true;
        LOG_debug << httpctx->getLogName() << "Range header detected";
    }

    return 0;
}

int MegaHTTPServer::onHeaderValue(http_parser *parser, const char *at, size_t length)
{
    MegaHTTPContext* httpctx = (MegaHTTPContext*) parser->data;
    string value(at, length);
    size_t index;
    char *endptr;

    LOG_verbose << httpctx->getLogName() << " onHeaderValue: " << httpctx->lastheader << " = "
                << value;
    if (httpctx->lastheader == "depth")
    {
        httpctx->depth = atoi(value.c_str());
    }
    else if (httpctx->lastheader == "host")
    {
        httpctx->host = value;
    }
    else if (httpctx->lastheader == "destination")
    {
        httpctx->destination = value;
    }
    else if (httpctx->lastheader == "overwrite")
    {
        httpctx->overwrite = (value == "T");
    }
    else if (httpctx->range)
    {
        LOG_debug << httpctx->getLogName() << "Range header value: " << value;
        httpctx->range = false;
        if (Utils::startswith(at, "bytes=") && ((index = value.find_first_of('-')) != string::npos))
        {
            endptr = (char *)value.c_str();
            unsigned long long number = strtoull(value.c_str() + 6, &endptr, 10);
            if (endptr == value.c_str() || *endptr != '-' || number == ULLONG_MAX)
            {
                return 0;
            }

            httpctx->rangeStart = static_cast<m_off_t>(number);
            if (length > (index + 1))
            {
                number = strtoull(value.c_str() + index + 1, &endptr, 10);
                if (endptr == value.c_str() || *endptr != '\0' || number == ULLONG_MAX)
                {
                    return 0;
                }
                httpctx->rangeEnd = static_cast<m_off_t>(number);
            }
            LOG_debug << httpctx->getLogName() << "Range value parsed: " << httpctx->rangeStart
                      << " - " << httpctx->rangeEnd;
        }
    }
    return 0;
}

int MegaHTTPServer::onBody(http_parser *parser, const char *b, size_t n)
{
    MegaHTTPContext* httpctx = (MegaHTTPContext*) parser->data;

    if (parser->method == HTTP_PUT)
    {
        //create tmp file with contents in messageBody
        if (!httpctx->tmpFileAccess)
        {
            httpctx->tmpFileName=httpctx->server->basePath;
            httpctx->tmpFileName.append("httputfile");
            httpctx->tmpFileName.append(LocalPath::tmpNameLocal().toPath(false));

            string ext;
            LocalPath localpath = LocalPath::fromAbsolutePath(httpctx->path);
            if (httpctx->server->fsAccess->getextension(localpath, ext))
            {
                httpctx->tmpFileName.append(ext);
            }

            httpctx->tmpFileAccess = httpctx->server->fsAccess->newfileaccess();
            LocalPath localPath = LocalPath::fromAbsolutePath(httpctx->tmpFileName);
            httpctx->server->fsAccess->unlinklocal(localPath);
            if (!httpctx->tmpFileAccess->fopen(localPath, OPEN_WRONLY, FSLogging::logOnError))
            {
                returnHttpCode(httpctx, 500); //is it ok to have a return here (not int onMessageComplete)?
                return 0;
            }
        }

        if (!httpctx->tmpFileAccess->fwrite(reinterpret_cast<const byte*>(b),
                                            static_cast<unsigned>(n),
                                            static_cast<m_off_t>(httpctx->messageBodySize)))
        {
            returnHttpCode(httpctx, 500);
            return 0;
        }
        httpctx->messageBodySize += n;
    }
    else
    {
        char *newbody = new char[n + httpctx->messageBodySize];
        memcpy(newbody, httpctx->messageBody, httpctx->messageBodySize);
        memcpy(newbody + httpctx->messageBodySize, b, n);
        httpctx->messageBodySize += n;
        delete [] httpctx->messageBody;
        httpctx->messageBody = newbody;
    }
    return 0;
}

string MegaHTTPServer::getWebDavProfFindNodeContents(MegaNode *node, string baseURL, bool offlineAttribute)
{
    std::ostringstream web;

    web << "<d:response>\r\n"
           "<d:href>" << webdavurlescape(baseURL) << "</d:href>\r\n"
           "<d:propstat>\r\n"
           "<d:status>HTTP/1.1 200 OK</d:status>\r\n"
           "<d:prop>\r\n"
           "<d:displayname>"<< webdavnameescape(node->getName()) << "</d:displayname>\r\n"

           "<d:creationdate>" << rfc1123_datetime(node->getCreationTime()) << "</d:creationdate>"
           "<d:getlastmodified>" << rfc1123_datetime(node->getModificationTime()) << "</d:getlastmodified>"
           ;

    if (offlineAttribute)
    {
        //(perhaps this could be based on number of files / or even better: size)
          web << "<Z:Win32FileAttributes>00001000</Z:Win32FileAttributes> \r\n"; //FILE_ATTRIBUTE_OFFLINE
//        web << "<Z:Win32FileAttributes>00040000</Z:Win32FileAttributes> \r\n"; // FILE_ATTRIBUTE_RECALL_ON_DATA_ACCESS (no actual difference)
    }

    if (node->isFolder())
    {
        web << "<d:resourcetype>\r\n"
               "<d:collection />\r\n"
               "</d:resourcetype>\r\n";
    }
    else
    {
        web << "<d:resourcetype />\r\n";
        web << "<d:getcontentlength>" << node->getSize() << "</d:getcontentlength>\r\n";
    }
    web << "</d:prop>\r\n"
           "</d:propstat>\r\n";
    web << "</d:response>\r\n";
    return web.str();
}

string MegaHTTPServer::getWebDavPropFindResponseForNode(string baseURL, string subnodepath, MegaNode *node, MegaHTTPContext* httpctx)
{
    std::ostringstream response;
    std::ostringstream web;

    web << "<?xml version=\"1.0\" encoding=\"utf-8\"?>\r\n"
           "<d:multistatus xmlns:d=\"DAV:\" xmlns:Z=\"urn:schemas-microsoft-com::\">\r\n";

    string subbaseURL = baseURL + subnodepath;
    if (node->isFolder() && subbaseURL.size() && subbaseURL.at(subbaseURL.size() - 1) != '/')
    {
        subbaseURL.append("/");
    }
    MegaHTTPServer* httpserver = dynamic_cast<MegaHTTPServer *>(httpctx->server);

    web << getWebDavProfFindNodeContents(node, subbaseURL, httpserver->isOfflineAttributeEnabled());
    if (node->isFolder() && (httpctx->depth != 0))
    {
        MegaNodeList *children = httpctx->megaApi->getChildren(node, MegaApi::ORDER_NONE);
        for (int i = 0; i < children->size(); i++)
        {
            MegaNode *child = children->get(i);
            string childURL = subbaseURL + child->getName();
            web << getWebDavProfFindNodeContents(child, childURL, httpserver->isOfflineAttributeEnabled());
        }
        delete children;
    }

    web << "</d:multistatus>"
           "\r\n";

    string sweb = web.str();
    response << "HTTP/1.1 207 Multi-Status\r\n"
                "content-length: " << sweb.size() << "\r\n"
                                                     "content-type: application/xml; charset=utf-8\r\n"
                                                     "server: MEGAsdk\r\n"
                                                     "\r\n";
    response << sweb;
    httpctx->resultCode = API_OK;
    return response.str();
}

string MegaHTTPServer::getResponseForNode(MegaNode *node, MegaHTTPContext* httpctx)
{
    MegaNode *parent = httpctx->megaApi->getParentNode(node);
    MegaNodeList *children = httpctx->megaApi->getChildren(node, MegaApi::ORDER_NONE);
    std::ostringstream response;
    std::ostringstream web;

    // Title
    web << "<title>MEGA</title>";

    // Styles
    web << "<head><meta charset=\"utf-8\" /><style>"
           ".folder {"
           "padding: 0;"
           "width: 24px;"
           "height: 24px;"
           "margin: 0 0 0 -2px;"
           "display: block;"
           "position: absolute;"
           "background-image: url(https://eu.static.mega.co.nz/3/images/mega/nw-fm-sprite_v12.svg);"
           "background-position: -14px -7465px;"
           "background-repeat: no-repeat;}"

           ".file {"
           "padding: 0;"
           "width: 24px;"
           "height: 24px;"
           "margin: 0 0 0 -6px;"
           "display: block;"
           "position: absolute;"
           "background-image: url(https://eu.static.mega.co.nz/3/images/mega/nw-fm-sprite_v12.svg);"
           "background-position: -7px -1494px;"
           "background-repeat: no-repeat;} "

           ".headerimage {"
           "padding: 0 8px 0 46px;"
           "width: 100%;"
           "height: 24px;"
           "margin: 0 0 0 -12px;"
           "display: block;"
           "position: absolute;"
           "background-image: url(https://eu.static.mega.co.nz/3/images/mega/nw-fm-sprite_v12.svg);"
           "background-position: 5px -1000px;"
           "line-height: 23px;"
           "background-repeat: no-repeat;} "

           ".headertext {"
           "line-height: 23px;"
           "color: #777777;"
           "font-size: 18px;"
           "font-weight: bold;"
           "display: block;"
           "position: absolute;"
           "line-height: 23px;}"

           "a {"
           "text-decoration: none; }"

           ".text {"
           "height: 24px;"
           "padding: 0 10px 0 26px;"
           "word-break: break-all;"
           "white-space: pre-wrap;"
           "overflow: hidden;"
           "max-width: 100%;"
           "text-decoration: none;"
           "-moz-box-sizing: border-box;"
           "-webkit-box-sizing: border-box;"
           "box-sizing: border-box;"
           "font-size: 13px;"
           "line-height: 23px;"
           "color: #666666;}"
           "</style></head>";

    // Folder path
    web << "<span class=\"headerimage\"><span class=\"headertext\">";
    char *path = httpctx->megaApi->getNodePath(node);
    if (path)
    {
        web << path;
        delete [] path;
    }
    else
    {
        web << node->getName();
    }
    web << "</span></span><br /><br />";

    // Child nodes
    web << "<table width=\"100%\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"width: auto;\">";
    if (parent)
    {
        web << "<tr><td>";
        char *base64Handle = parent->getBase64Handle();
        if (httpctx->megaApi->httpServerGetRestrictedMode() == MegaApi::TCP_SERVER_ALLOW_ALL)
        {
            web << "<a href=\"/" << base64Handle << "/" << parent->getName();
        }
        else
        {
            web << "<a href=\"" << "../" << parent->getName();
        }

        web << "\"><span class=\"folder\"></span><span class=\"text\">..</span></a>";
        delete [] base64Handle;
        delete parent;
        web << "</td></tr>";
    }

    for (int i = 0; i < children->size(); i++)
    {
        web << "<tr><td>";
        MegaNode *child = children->get(i);
        char *base64Handle = child->getBase64Handle();
        if (httpctx->megaApi->httpServerGetRestrictedMode() == MegaApi::TCP_SERVER_ALLOW_ALL)
        {
            web << "<a href=\"/" << base64Handle << "/" << child->getName();
        }
        else
        {
            web << "<a href=\"" << node->getName() << "/" << child->getName();
        }
        web << "\"><span class=\"" << (child->isFile() ? "file" : "folder") << "\"></span><span class=\"text\">"
            << child->getName() << "</span></a>";
        delete [] base64Handle;

        if (!child->isFile())
        {
            web << "</td><td>";
        }
        else
        {
            unsigned const long long KB = 1024;
            unsigned const long long MB = 1024 * KB;
            unsigned const long long GB = 1024 * MB;
            unsigned const long long TB = 1024 * GB;

            web << "</td><td><span class=\"text\">";
            unsigned long long bytes = static_cast<unsigned long long>(child->getSize());
            if (bytes > TB)
                web << double(((unsigned long long)((100 * bytes) / TB)))/100.0 << " TB";
            else if (bytes > GB)
                web << double(((unsigned long long)((100 * bytes) / GB)))/100.0 << " GB";
            else if (bytes > MB)
                web << double(((unsigned long long)((100 * bytes) / MB)))/100.0 << " MB";
            else if (bytes > KB)
                web << double(((unsigned long long)((100 * bytes) / KB)))/100.0 << " KB";
            web << "</span>";
        }
        web << "</td></tr>";
    }
    web << "</table>";
    delete children;

    string sweb = web.str();
    response << "HTTP/1.1 200 OK\r\n"
        << "Content-Type: text/html; charset=utf-8\r\n"
        << "Connection: close\r\n"
        << "Content-Length: " << sweb.size() << "\r\n"
        << "Access-Control-Allow-Origin: *\r\n"
        << "\r\n";

    if (httpctx->parser.method != HTTP_HEAD)
    {
        response << sweb;
    }
    httpctx->resultCode = API_OK;

    return response.str();
}

string MegaHTTPServer::getHTTPMethodName(int httpmethod)
{
    switch (httpmethod)
    {
    case HTTP_DELETE:
        return "HTTP_DELETE";
    case HTTP_GET:
        return "HTTP_GET";
    case HTTP_HEAD:
        return "HTTP_HEAD";
    case HTTP_POST:
        return "HTTP_POST";
    case HTTP_PUT:
        return "HTTP_PUT";
    case HTTP_CONNECT:
        return "HTTP_CONNECT";
    case HTTP_OPTIONS:
        return "HTTP_OPTIONS";
    case HTTP_TRACE:
        return "HTTP_TRACE";
    case HTTP_COPY:
        return "HTTP_COPY";
    case HTTP_LOCK:
        return "HTTP_LOCK";
    case HTTP_MKCOL:
        return "HTTP_MKCOL";
    case HTTP_MOVE:
        return "HTTP_MOVE";
    case HTTP_PROPFIND:
        return "HTTP_PROPFIND";
    case HTTP_PROPPATCH:
        return "HTTP_PROPPATCH";
    case HTTP_SEARCH:
        return "HTTP_SEARCH";
    case HTTP_UNLOCK:
        return "HTTP_UNLOCK";
    case HTTP_BIND:
        return "HTTP_BIND";
    case HTTP_REBIND:
        return "HTTP_REBIND";
    case HTTP_UNBIND:
        return "HTTP_UNBIND";
    case HTTP_ACL:
        return "HTTP_ACL";
    case HTTP_REPORT:
        return "HTTP_REPORT";
    case HTTP_MKACTIVITY:
        return "HTTP_MKACTIVITY";
    case HTTP_CHECKOUT:
        return "HTTP_CHECKOUT";
    case HTTP_MERGE:
        return "HTTP_MERGE";
    case HTTP_MSEARCH:
        return "HTTP_MSEARCH";
    case HTTP_NOTIFY:
        return "HTTP_NOTIFY";
    case HTTP_SUBSCRIBE:
        return "HTTP_SUBSCRIBE";
    case HTTP_UNSUBSCRIBE:
        return "HTTP_UNSUBSCRIBE";
    case HTTP_PATCH:
        return "HTTP_PATCH";
    case HTTP_PURGE:
        return "HTTP_PURGE";
    case HTTP_MKCALENDAR:
        return "HTTP_MKCALENDAR";
    case HTTP_LINK:
        return "HTTP_LINK";
    case HTTP_UNLINK:
        return "HTTP_UNLINK";
    default:
        return "HTTP_UNKOWN";
    }
}

string MegaHTTPServer::getHTTPErrorString(int errorcode)
{
    switch (errorcode)
    {
    case 200:
        return "OK";
    case 201:
        return "Created";
    case 204:
        return "No Content";
    case 403:
        return "Forbidden";
    case 404:
        return "Not Found";
    case 409:
        return "Conflict";
    case 412:
        return "Precondition Failed";
    case 423:
        return "Locked";
    case 500:
        return "Internal Server Error";
    case 502:
        return "Bad Gateway";
    case 503:
        return "Service Unavailable";
    case 507:
        return "Insufficient Storage";
    case 508:
        return "Loop Detected";
    default:
        return "Unknown Error";
    }
}

void MegaHTTPServer::returnHttpCodeBasedOnRequestError(MegaHTTPContext* httpctx, MegaError *e, bool synchronous)
{
    int reqError = e->getErrorCode();
    int httpreturncode = 500;

    switch(reqError)
    {
    case API_EACCESS:
        httpreturncode = 403;
        break;
    case API_EOVERQUOTA:
    case API_EGOINGOVERQUOTA:
        httpreturncode = 507;
        break;
    case API_EAGAIN:
    case API_ERATELIMIT:
    case API_ETEMPUNAVAIL:
        httpreturncode = 503;
        break;
    case API_ECIRCULAR:
        httpreturncode = 508;
        break;
    default:
        httpreturncode = 500;
        break;
    }

    LOG_debug << httpctx->getLogName() << "HTTP petition failed. request error = " << reqError
              << " HTTP status to return = " << httpreturncode;
    string errorMessage = e->getErrorString(reqError);
    return returnHttpCode(httpctx, httpreturncode, errorMessage, synchronous);
}

void MegaHTTPServer::returnHttpCode(MegaHTTPContext* httpctx, int errorCode, string errorMessage, bool synchronous)
{
    std::ostringstream response;
    response << "HTTP/1.1 " << errorCode << " "
             << (errorMessage.size() ? errorMessage : getHTTPErrorString(errorCode))
             << "\r\n"
                "Connection: close\r\n"
              << "\r\n";

    httpctx->resultCode = errorCode;
    string resstr = response.str();
    if (synchronous)
    {
        sendHeaders(httpctx, &resstr);
    }
    else
    {
        uv_mutex_lock(&httpctx->mutex_responses);
        httpctx->responses.push_back(resstr);
        uv_mutex_unlock(&httpctx->mutex_responses);
        uv_async_send(&httpctx->asynchandle);
    }
}

void MegaHTTPServer::returnHttpCodeAsyncBasedOnRequestError(MegaHTTPContext* httpctx, MegaError *e)
{
    return returnHttpCodeBasedOnRequestError(httpctx, e, false);
}

void MegaHTTPServer::returnHttpCodeAsync(MegaHTTPContext* httpctx, int errorCode, string errorMessage)
{
    return returnHttpCode(httpctx, errorCode, errorMessage, false);
}

int MegaHTTPServer::onMessageComplete(http_parser *parser)
{
    MegaNode *node = NULL;
    std::ostringstream response;
    MegaHTTPContext* httpctx = (MegaHTTPContext*) parser->data;
    LOG_debug << httpctx->getLogName() << "Message complete";
    httpctx->bytesWritten = 0;
    httpctx->size = 0;
    httpctx->streamingBuffer.setMaxBufferSize(
        static_cast<unsigned>(httpctx->server->getMaxBufferSize()));
    httpctx->streamingBuffer.setMaxOutputSize(
        static_cast<unsigned>(httpctx->server->getMaxOutputSize()));

    MegaHTTPServer* httpserver = dynamic_cast<MegaHTTPServer *>(httpctx->server);

    switch (parser->method)
    {
    case HTTP_GET:
    case HTTP_POST:
    case HTTP_HEAD:
    case HTTP_OPTIONS:
    case HTTP_MOVE:
    case HTTP_PUT:
    case HTTP_DELETE:
    case HTTP_MKCOL:
    case HTTP_COPY:
    case HTTP_LOCK:
    case HTTP_UNLOCK:
    case HTTP_PROPPATCH:
    case HTTP_PROPFIND:
        LOG_debug << httpctx->getLogName()
                  << "Request method: " << getHTTPMethodName(parser->method);
        break;
    default:
        LOG_debug << httpctx->getLogName()
                  << "Method not allowed: " << getHTTPMethodName(parser->method);
        response << "HTTP/1.1 405 Method not allowed\r\n"
                    "Connection: close\r\n"
                    "\r\n";

        httpctx->resultCode = 405;
        string resstr = response.str();
        sendHeaders(httpctx, &resstr);
        return 0;
    }

    if (httpctx->path == "/favicon.ico")
    {
        LOG_debug << httpctx->getLogName() << "Favicon requested";
        response << "HTTP/1.1 301 Moved Permanently\r\n"
                    "Location: ";
        response << MegaClient::getMegaURL();
        response << "/favicon.ico\r\n"
                    "Connection: close\r\n"
                    "\r\n";

        httpctx->resultCode = API_OK;
        string resstr = response.str();
        sendHeaders(httpctx, &resstr);
        return 0;
    }

    if (httpctx->path == "/")
    {
        node = httpctx->megaApi->getRootNode();
        char *base64Handle = node->getBase64Handle();
        httpctx->nodehandle = base64Handle;
        delete [] base64Handle;
        httpctx->nodename = node->getName();
    }
    else if (httpctx->nodehandle.size())
    {
        node = httpctx->megaApi->getNodeByHandle(MegaApi::base64ToHandle(httpctx->nodehandle.c_str()));
    }

    if (!httpctx->nodehandle.size())
    {
        response << "HTTP/1.1 404 Not Found\r\n"
                    "Connection: close\r\n"
                  << "\r\n";
        httpctx->resultCode = 404;
        string resstr = response.str();
        sendHeaders(httpctx, &resstr);
        delete node;
        return 0;
    }

    handle h = MegaApi::base64ToHandle(httpctx->nodehandle.c_str());
    if (!httpctx->server->isHandleAllowed(h))
    {
        LOG_debug << httpctx->getLogName() << "Forbidden due to the restricted mode";
        response << "HTTP/1.1 403 Forbidden\r\n"
                    "Connection: close\r\n"
                  << "\r\n";

        httpctx->resultCode = 403;
        string resstr = response.str();
        sendHeaders(httpctx, &resstr);
        delete node;
        return 0;
    }

    if (parser->method == HTTP_OPTIONS)
    {
        LOG_debug << httpctx->getLogName() << "Returning HTTP_OPTIONS for a "
                  << (httpserver->isHandleWebDavAllowed(h) ? "" : "non ") << "WEBDAV URI";
        response << "HTTP/1.1 200 OK\r\n";

        if (httpserver->isHandleWebDavAllowed(h))
        {
            response << "Allow: GET, POST, HEAD, OPTIONS, PROPFIND, MOVE, PUT, DELETE, MKCOL, COPY, LOCK, UNLOCK, PROPPATCH\r\n"
                        "dav: 1, 2 \r\n"; // 2 requires LOCK to be fully functional
        }
        else
        {
            response << "Allow: GET,POST,HEAD,OPTIONS\r\n";
        }

        response << "content-length: 0\r\n"
        "Connection: close\r\n"
        "\r\n";

        httpctx->resultCode = API_OK;
        string resstr = response.str();
        sendHeaders(httpctx, &resstr);
        delete node;
        return 0;
    }

    //if webdav method, check is handle is a valid webdav
    if ((parser->method != HTTP_GET) && (parser->method != HTTP_POST)
            && (parser->method != HTTP_PUT) && (parser->method != HTTP_HEAD)
            && !httpserver->isHandleWebDavAllowed(h))
    {
        LOG_debug << httpctx->getLogName() << "Forbidden due to not webdav allowed";
        returnHttpCode(httpctx, 405);
        delete node;
        return 0;
    }

    if (!node)
    {
        if (!httpctx->nodehandle.size() || !httpctx->nodekey.size())
        {
            LOG_warn << httpctx->getLogName() << "URL not found: " << httpctx->path;
            response << "HTTP/1.1 404 Not Found\r\n"
                        "Connection: close\r\n"
                      << "\r\n";

            httpctx->resultCode = 404;
            string resstr = response.str();
            sendHeaders(httpctx, &resstr);
            return 0;
        }
        else if (httpctx->nodesize >= 0)
        {
            LOG_debug << httpctx->getLogName() << "Getting foreign node";
            node = httpctx->megaApi->createForeignFileNode(
                        h, httpctx->nodekey.c_str(),
                        httpctx->nodename.c_str(),
                        httpctx->nodesize,
                        -1, nullptr, UNDEF, httpctx->nodeprivauth.c_str(), httpctx->nodepubauth.c_str(), httpctx->nodechatauth.c_str());
        }
        else
        {
            handle httpNodeHandle = MegaApi::base64ToHandle(httpctx->nodehandle.c_str());
            node = MegaHTTPContext::publicNodes.getCopy(httpNodeHandle).release();
            if (!node)
            {
                // Send a request to the server to get its data
                string link =
                    MegaClient::publicLinkURL(httpctx->megaApi->getMegaClient()->mNewLinkFormat,
                                              TypeOfLink::FILE,
                                              httpNodeHandle,
                                              httpctx->nodekey.c_str());
                LOG_debug << httpctx->getLogName() << "Getting public link: " << link;
                httpctx->megaApi->getPublicNode(link.c_str(), httpctx);
                // getPublicNode result is processed inside httpctx onRequestFinish
                return 0;
            }
        }
    }

    if (node && httpctx->nodename != node->getName())
    {
        if (parser->method == HTTP_PROPFIND)
        {
            response << "HTTP/1.1 404 Not Found\r\n"
                        "Connection: close\r\n"
                     << "\r\n";

            httpctx->resultCode = 404;
            string resstr = response.str();
            sendHeaders(httpctx, &resstr);
            delete node;
            return 0;
        }
        else
        {
            //Subtitles support
            bool subtitles = false;

            if (httpserver->isSubtitlesSupportEnabled())
            {
                string originalname = node->getName();
                string::size_type dotpos = originalname.find_last_of('.');
                if (dotpos != string::npos)
                {
                    originalname.resize(dotpos);
                }

                if (dotpos == httpctx->nodename.find_last_of('.') && !memcmp(originalname.data(), httpctx->nodename.data(), originalname.size()))
                {
                    LOG_debug << httpctx->getLogName() << "Possible subtitles file";
                    MegaNode *parent = httpctx->megaApi->getParentNode(node);
                    if (parent)
                    {
                        MegaNode *child = httpctx->megaApi->getChildNode(parent, httpctx->nodename.c_str());
                        if (child)
                        {
                            LOG_debug << httpctx->getLogName()
                                      << "Matching file found: " << httpctx->nodename << " - "
                                      << node->getName();
                            subtitles = true;
                            delete node;
                            node = child;
                        }
                        delete parent;
                    }
                }
            }

            if (!subtitles)
            {
                LOG_warn << httpctx->getLogName() << "Invalid name: " << httpctx->nodename << " - "
                         << node->getName();
                response << "HTTP/1.1 404 Not Found\r\n"
                            "Connection: close\r\n"
                         << "\r\n";

                httpctx->resultCode = 404;
                string resstr = response.str();
                sendHeaders(httpctx, &resstr);
                delete node;
                return 0;
            }
        }
    }

    MegaNode *baseNode = NULL;
    if (httpctx->subpathrelative.size())
    {
        string subnodepath = httpctx->subpathrelative;
        //remove trailing "/"
        size_t seppos = subnodepath.find_last_of("/");
        while ( (seppos != string::npos) && ((seppos + 1) == subnodepath.size()) )
        {
            subnodepath = subnodepath.substr(0,seppos);
            seppos = subnodepath.find_last_of("/");
        }

        MegaNode *subnode = httpctx->megaApi->getNodeByPath(subnodepath.c_str(), node);
        if (parser->method != HTTP_PUT && parser->method != HTTP_MKCOL && !subnode)
        {
            returnHttpCode(httpctx, 404);
            delete node;
            return 0;
        }
        else
        {
            baseNode = node;
            node = subnode;
        }
    }

    if (parser->method == HTTP_PROPFIND)
    {
        string baseURL = string("http") + (httpctx->server->useTLS ? "s" : "") + "://"
                + httpctx->host + "/" + httpctx->nodehandle + "/" + httpctx->nodename + "/";
        string resstr = getWebDavPropFindResponseForNode(baseURL, httpctx->subpathrelative, node, httpctx);
        sendHeaders(httpctx, &resstr);
        delete node;
        delete baseNode;
        return 0;
    }
    else if (parser->method == HTTP_UNLOCK)
    {
        // let's create a minimum unlock compliant response
        returnHttpCode(httpctx, 204);
        delete node;
        delete baseNode;
        return 0;
    }
    else if (parser->method == HTTP_PROPPATCH)
    {
        std::ostringstream web;

//        Typicall Body received: //TODO: actualy update creation and mod times? does not seem to be required (it seems PUT updates them ... with what time: file or PUT time?)
//        <?xml version="1.0" encoding="utf-8" ?>
//        <D:propertyupdate xmlns:D="DAV:" xmlns:Z="urn:schemas-microsoft-com:">
//        <D:set>
//        <D:prop>
//        <Z:Win32CreationTime>Tue, 20 Feb 2018 18:00:20 GMT</Z:Win32CreationTime>
//        <Z:Win32LastAccessTime>Tue, 20 Feb 2018 18:00:21 GMT</Z:Win32LastAccessTime>
//        <Z:Win32LastModifiedTime>Tue, 20 Feb 2018 18:00:21 GMT</Z:Win32LastModifiedTime>
//        <Z:Win32FileAttributes>00000020</Z:Win32FileAttributes>
//        </D:prop>
//        </D:set>
//        </D:propertyupdate>

        web << "<?xml version=\"1.0\" encoding=\"utf-8\" ?>\r\n"
               "<d:multistatus xmlns:d=\"DAV:\">\r\n"
                  "<d:response>\r\n"
                  //"<d:href>" << urlelement << "</d:href>\r\n" //this should come from the input but seems to be not required!
                    "<d:propstat>\r\n"
                      "<d:status>HTTP/1.1 200 OK</d:status>\r\n"
                        "<d:prop>\r\n"
                        // Here we might want to include the updated properties.
                        //we might want to to do so with original namespace?(not sure if that makes sense). e.g:
//                                 "<Z:Win32CreationTime/>\r\n"
                        "</d:prop>\r\n"
                    "</d:propstat>\r\n"
                  "</d:response>\r\n"
                "</d:multistatus>\r\n\r\n";

        string sweb = web.str();

        response << "HTTP/1.1 207 Multi-Status\r\n"
                    "Content-Type: application/xml; charset=\"utf-8\" \r\n"
                    "Content-Length: " << sweb.size() << "\r\n\r\n";

        response << sweb;
        httpctx->resultCode = 207;
        string resstr = response.str();
        sendHeaders(httpctx, &resstr);
        delete node;
        delete baseNode;
        return 0;
    }
    else if (parser->method == HTTP_LOCK)
    {
        std::ostringstream web;

        // let's create a minimum lock compliant response. we should actually provide locking functionality based on URL
        web << "<?xml version=\"1.0\" encoding=\"utf-8\" ?>\r\n"
          "<D:prop xmlns:D=\"DAV:\">\r\n"
            "<D:lockdiscovery>\r\n"
              "<D:activelock>\r\n"
                "<D:locktype><D:write/></D:locktype>\r\n"
                "<D:lockscope><D:exclusive/></D:lockscope>\r\n"
//                "<D:depth>infinity</D:depth>\r\n" // read from req?
                "<D:owner>\r\n"
//                  "<D:href>" << owner << "</D:href>\r\n" // should be read from req body
                "</D:owner>\r\n"
//                "<D:timeout>Second-604800</D:timeout>\r\n"
                "<D:locktoken>\r\n"
                  //"<D:href>urn:uuid:e71d4fae-5dec-22d6-fea5-00a0c91e6be4</D:href>\r\n" //An unique identifier is required
               "<D:href>urn:uuid:this-is-a-fake-lock</D:href>\r\n" //An unique identifier is required
                "</D:locktoken>\r\n"
                "<D:lockroot>\r\n"
//                  "<D:href>" << urlelement << "</D:href>\r\n"  // should be read from req body
                "</D:lockroot>\r\n"
              "</D:activelock>\r\n"
            "</D:lockdiscovery>\r\n"
          "</D:prop>\r\n\r\n";

        string sweb = web.str();
        response << "HTTP/1.1 200 OK\r\n"
          "Lock-Token: <urn:uuid:e71d4fae-5dec-22d6-fea5-00a0c91e6be4> \r\n"
          "Content-Type: application/xml; charset=\"utf-8\" \r\n"
          "Content-Length: " << sweb.size() << "\r\n\r\n";

        response << sweb;
        httpctx->resultCode = 200;
        string resstr = response.str();
        sendHeaders(httpctx, &resstr);
        delete node;
        delete baseNode;
        return 0;
    }
    else if (parser->method == HTTP_DELETE)
    {
        if (!node)
        {
            returnHttpCode(httpctx, 404);
            delete node;
            delete baseNode;
            return 0;
        }

        httpctx->megaApi->remove(node, false, httpctx);
        delete node;
        delete baseNode;
        return 0;
    }
    else if (parser->method == HTTP_MKCOL)
    {
//        201 (Created)	The collection was created.
//        401 (Access Denied)	Resource requires authorization or authorization was denied.
//        403 (Forbidden)	The server does not allow collections to be created at the specified location, or the parent collection of the specified request URI exists but cannot accept members.
//        405 (Method Not Allowed)	The MKCOL method can only be performed on a deleted or non-existent resource.
//        409 (Conflict)	A resource cannot be created at the destination URI until one or more intermediate collections are created.
//        415 (Unsupported Media Type)	The request type of the body is not supported by the server.
//        507 (Insufficient Storage)	The destination resource does not have sufficient storage space.
        if (node)
        {
            returnHttpCode(httpctx, 405);
            delete node;
            delete baseNode;
            return 0;
        }

        MegaNode *newParentNode = NULL;
        string newname;

        string dest = httpctx->subpathrelative;
        size_t seppos = dest.find_last_of("/");

        while ( (seppos != string::npos) && ((seppos + 1) == dest.size()) )
        {
            dest = dest.substr(0,seppos);
            seppos = dest.find_last_of("/");
        }
        if (seppos == string::npos)
        {
            if (baseNode)  newParentNode = baseNode->copy();
            else if (node) newParentNode = node->copy();
            else           newParentNode = nullptr;
            newname = dest;
        }
        else
        {
            if ((seppos + 1) < dest.size())
            {
                newname = dest.substr(seppos + 1);
            }
            string newparentpath = dest.substr(0, seppos);
            newParentNode = httpctx->megaApi->getNodeByPath(newparentpath.c_str(),baseNode?baseNode:node);
        }
        if (!newParentNode)
        {
            returnHttpCode(httpctx, 409);
            delete node;
            delete baseNode;
            return 0;
        }

        httpctx->megaApi->createFolder(newname.c_str(), newParentNode, httpctx);
        delete newParentNode;
        delete node;
        delete baseNode;
        return 0;
    }
    else if (parser->method == HTTP_COPY)
    {
//        201 (Created)	The resource was successfully copied.
//        204 (No Content)	The source resource was successfully copied to a pre-existing destination resource.
//        403 (Forbidden)	The source URI and the destination URI are the same.
//        409 (Conflict)	A resource cannot be created at the destination URI until one or more intermediate collections are created.
//        412 (Precondition Failed)	Either the Overwrite header is "F" and the state of the destination resource is not null, or the method was used in a Depth: 0 transaction.
//        423 (Locked)	The destination resource is locked.
//        502 (Bad Gateway)	The COPY destination is located on a different server, which refuses to accept the resource.
//        507 (Insufficient Storage)	The destination resource does not have sufficient storage space.

        if (!node)
        {
            returnHttpCode(httpctx, 404);
            delete node;
            delete baseNode;
            return 0;
        }

        MegaNode *newParentNode = NULL;
        string newname;
        string baseURL = string("http") + (httpctx->server->useTLS ? "s" : "") + "://"
                + httpctx->host + "/" + httpctx->nodehandle + "/" + httpctx->nodename + "/";

        string dest;
        URLCodec::unescape(&httpctx->destination, &dest);
        size_t posBase = dest.find(baseURL);
        if (posBase != 0) // Notice that if 2 WEBDAV locations are enabled we won't be able to copy between the 2
        {
            returnHttpCode(httpctx, 502); // The destination URI is located elsewhere
            delete node;
            delete baseNode;
            return 0;
        }

        dest = dest.substr(baseURL.size());
        MegaNode *destNode = httpctx->megaApi->getNodeByPath(dest.c_str(), baseNode ? baseNode : node);
        if (destNode)
        {
            if (node->getHandle() == destNode->getHandle())
            {
                returnHttpCode(httpctx, 403);
                delete node;
                delete baseNode;
                delete destNode;
                return 0;
            }
            else
            {
                //overwrite?
                if (httpctx->overwrite)
                {
                    newParentNode = httpctx->megaApi->getNodeByHandle(destNode->getParentHandle());
                }
                else
                {
                    returnHttpCode(httpctx, 412);
                    delete node;
                    delete baseNode;
                    delete destNode;
                    return 0;
                }
            }
        }

        if (!newParentNode)
        {
            size_t seppos = dest.find_last_of("/");
            while ( (seppos != string::npos) && ((seppos + 1) == dest.size()) )
            {
                dest = dest.substr(0,seppos);
                seppos = dest.find_last_of("/");
            }
            if (seppos == string::npos)
            {
                newParentNode = baseNode?baseNode->copy():node->copy();
                newname = dest;
            }
            else
            {
                if ((seppos + 1) < dest.size())
                {
                    newname = dest.substr(seppos + 1);
                }
                string newparentpath = dest.substr(0, seppos);
                newParentNode = httpctx->megaApi->getNodeByPath(newparentpath.c_str(),baseNode?baseNode:node);
            }
        }
        if (!newParentNode)
        {
            returnHttpCode(httpctx, 409);
            delete node;
            delete baseNode;
            return 0;
        }
        if (newname.size())
        {
            httpctx->megaApi->copyNode(node, newParentNode, newname.c_str(), httpctx);
        }
        else
        {
            httpctx->megaApi->copyNode(node, newParentNode, httpctx);
        }

        delete node;
        delete baseNode;
        delete newParentNode;
        return 0;
    }
    else if (parser->method == HTTP_PUT)
    {
        if (node && !httpctx->overwrite)
        {
            returnHttpCode(httpctx, 412);
            delete node;
            delete baseNode;
            return 0;
        }
        else
        {
            MegaNode *newParentNode = NULL;
            string newname;

            string dest = httpctx->subpathrelative;
            size_t seppos = dest.find_last_of("/");
            while ( (seppos != string::npos) && ((seppos + 1) == dest.size()) )
            {
                dest = dest.substr(0,seppos);
                seppos = dest.find_last_of("/");
            }
            if (seppos == string::npos)
            {
                newParentNode = baseNode ? baseNode->copy() : node->copy();
                newname = dest;
            }
            else
            {
                if ((seppos + 1) < dest.size())
                {
                    newname = dest.substr(seppos + 1);
                }
                string newparentpath = dest.substr(0, seppos);
                newParentNode = httpctx->megaApi->getNodeByPath(newparentpath.c_str(), baseNode ? baseNode : node);
            }

            if (!newParentNode)
            {
                returnHttpCode(httpctx, 409);
                delete node;
                delete baseNode;
                return 0;
            }

            if (!httpctx->tmpFileAccess) //put with no body contents
            {
                httpctx->tmpFileName=httpctx->server->basePath;
                httpctx->tmpFileName.append("httputfile");
                httpctx->tmpFileName.append(LocalPath::tmpNameLocal().toPath(false));
                string ext;
                if (httpctx->server->fsAccess->getextension(LocalPath::fromAbsolutePath(httpctx->path), ext))
                {
                    httpctx->tmpFileName.append(ext);
                }
                httpctx->tmpFileAccess = httpctx->server->fsAccess->newfileaccess();
                auto tmpFileNamePath = LocalPath::fromAbsolutePath(httpctx->tmpFileName);
                httpctx->server->fsAccess->unlinklocal(tmpFileNamePath);
                if (!httpctx->tmpFileAccess->fopen(tmpFileNamePath,
                                                   OPEN_WRONLY,
                                                   FSLogging::logOnError))
                {
                    returnHttpCode(httpctx, 500);
                    delete node;
                    delete baseNode;
                    delete newParentNode;
                    return 0;
                }
            }

            FileSystemType fsType = httpctx->server->fsAccess->getlocalfstype(LocalPath::fromAbsolutePath(httpctx->tmpFileName));
            MegaApiImpl::MegaUploadOptionsPrivate uploadOptions;
            uploadOptions.mPublicOptions.fileName = newname;
            uploadOptions.mPublicOptions.mtime = -1;
            uploadOptions.mIsBackup = true;
            uploadOptions.mFsType = fsType;

            httpctx->megaApi->startUpload(httpctx->tmpFileName,
                                          newParentNode->getHandle(),
                                          CancelToken(),
                                          uploadOptions,
                                          httpctx);

            delete node;
            delete baseNode;
            delete newParentNode;
            return 0;
        }
    }
    else if (parser->method == HTTP_MOVE)
    {
        //        201 (Created)	The resource was moved successfully and a new resource was created at the specified destination URI.
        //        204 (No Content)	The resource was moved successfully to a pre-existing destination URI.
        //        403 (Forbidden)	The source URI and the destination URI are the same.
        //        409 (Conflict)	A resource cannot be created at the destination URI until one or more intermediate collections are created.
        //        412 (Precondition Failed)	Either the Overwrite header is "F" and the state of the destination resource is not null, or the method was used in a Depth: 0 transaction.
        //        423 (Locked)	The destination resource is locked.
        //        502 (Bad Gateway)	The destination URI is located on a different server, which refuses to accept the resource.

        if (!node)
        {
            returnHttpCode(httpctx, 404);
            delete node;
            delete baseNode;
            return 0;
        }

        string baseURL = string("http") + (httpctx->server->useTLS ? "s" : "") + "://" + httpctx->host
                + "/" + httpctx->nodehandle + "/" + httpctx->nodename + "/";

        string dest;
        URLCodec::unescape(&httpctx->destination, &dest);
        size_t posBase = dest.find(baseURL);
        if (posBase != 0) // Notice that if 2 WEBDAV locations are enabled we won't be able to copy between the 2
        {
            returnHttpCode(httpctx, 502); // The destination URI is located elsewhere
            delete node;
            delete baseNode;
            return 0;
        }
        dest = dest.substr(baseURL.size());
        MegaNode *destNode = httpctx->megaApi->getNodeByPath(dest.c_str(), baseNode ? baseNode : node);
        if (destNode)
        {
            if (node->getHandle() == destNode->getHandle())
            {
                returnHttpCode(httpctx, 403);
                delete node;
                delete baseNode;
                delete destNode;
                return 0;
            }
            else
            {
                //overwrite?
                if (httpctx->overwrite)
                {
                    httpctx->newParentNode = destNode->getParentHandle();
                    httpctx->newname = destNode->getName();
                    httpctx->nodeToMove = node->getHandle();
                    httpctx->megaApi->remove(destNode, false, httpctx);
                    delete node;
                    delete baseNode;
                    delete destNode;
                    return 0;
                }
                else
                {
                    returnHttpCode(httpctx, 412);
                    delete node;
                    delete baseNode;
                    delete destNode;
                    return 0;
                }
            }
        }
        else
        {
            MegaNode *newParentNode = NULL;
            size_t seppos = dest.find_last_of("/");
            httpctx->newname = dest;
            if (seppos == string::npos)
            {
                newParentNode = baseNode ? baseNode->copy() : node->copy();
            }
            else
            {
                if ((seppos + 1) < httpctx->newname.size())
                {
                    httpctx->newname = httpctx->newname.substr(seppos + 1);
                }
                string newparentpath = dest.substr(0, seppos);
                newParentNode = httpctx->megaApi->getNodeByPath(newparentpath.c_str(), baseNode ? baseNode : node);
            }
            if (!newParentNode)
            {
                returnHttpCode(httpctx, 409);
                delete node;
                delete baseNode;
                return 0;
            }

            if (newParentNode->getHandle() == node->getHandle())
            {
                LOG_warn << httpctx->getLogName() << "HTTP_MOVE trying to mov a node into itself";
                returnHttpCode(httpctx, 500);
                delete node;
                delete baseNode;
                delete newParentNode;
                return 0;
            }

            if (newParentNode->getHandle() != node->getParentHandle())
            {
                httpctx->megaApi->moveNode(node, newParentNode, httpctx);
            }
            else
            {
                httpctx->megaApi->renameNode(node, httpctx->newname.c_str(), httpctx);
            }
            delete newParentNode;
        }

        delete node;
        delete baseNode;
        return 0;
    }
    else //GET/POST/HEAD
    {
        if (node->isFolder())
        {
            if (!httpserver->isFolderServerEnabled())
            {
                response << "HTTP/1.1 403 Forbidden\r\n"
                            "Connection: close\r\n"
                          << "\r\n";

                httpctx->resultCode = 403;
                string resstr = response.str();
                sendHeaders(httpctx, &resstr);
                delete node;
                delete baseNode;
                return 0;
            }

            string resstr = getResponseForNode(node, httpctx);

            sendHeaders(httpctx, &resstr);
            delete node;
            delete baseNode;
            return 0;
        }

        //File node
        if (!httpserver->isFileServerEnabled())
        {
            response << "HTTP/1.1 403 Forbidden\r\n"
                        "Connection: close\r\n"
                      << "\r\n";

            httpctx->resultCode = 403;
            string resstr = response.str();
            sendHeaders(httpctx, &resstr);
            delete node;
            delete baseNode;
            return 0;
        }

        delete httpctx->node;
        httpctx->node = node;
        streamNode(httpctx);
    }
    delete baseNode;
    return 0;
}

bool MegaHTTPServer::startStream(MegaHTTPContext* httpctx,
                                 std::uint64_t offset,
                                 std::uint64_t length)
{
    // Sanity: node should never be null.
    assert(httpctx->node);

    // Convenience.
    using file_service::FILE_SERVICE_FILE_DOESNT_EXIST;

    auto& node = static_cast<MegaNodePrivate&>(*httpctx->node);
    auto& service = httpctx->megaApi->getMegaClient()->mFileService;

    // True if this file isn't owned directly by us.
    auto external = node.isForeign() || node.isPublic();

    // Assume the file's already known to the service.
    auto handle = NodeHandle().set6byte(node.getHandle());
    auto id = FileID::from(handle);

    // Try and open the file for reading.
    auto file = service.open(id);

    // Couldn't open the file because it hasn't been added to the service.
    if (external && !file && file.error() == FILE_SERVICE_FILE_DOESNT_EXIST)
    {
        // Convenience.
        using common::fromCharPointer;
        using common::fromStringPointer;
        using common::NodeKeyData;
        using file_service::FILE_SERVICE_FILE_ALREADY_EXISTS;

        // Populate node key data.
        NodeKeyData keyData;

        // Populate authentication tokens.
        keyData.mChatAuth = fromCharPointer(node.getChatAuth());
        keyData.mPrivateAuth = fromStringPointer(node.getPrivateAuth());
        keyData.mPublicAuth = fromStringPointer(node.getPublicAuth());

        // Populate node key material.
        if (auto* keyAndIV = node.getNodeKey(); !keyAndIV->empty())
            keyData.mKeyAndIV = *keyAndIV;

        // Remember whether the node is public or not.
        keyData.mIsPublicHandle = node.isPublic();

        // Sanity: size should never be negative.
        assert(node.getSize() >= 0);

        // Convenience.
        auto size = static_cast<std::uint64_t>(node.getSize());

        // Try and add the node to the service.
        auto added = service.add(handle, keyData, size);

        // Couldn't add the node to the service.
        if (!added && added.error() != FILE_SERVICE_FILE_ALREADY_EXISTS)
        {
            LOG_err << "Failed to add file " << toNodeHandle(handle)
                    << " to the service: " << toString(added.error());

            return false;
        }

        // Try and open the file for reading.
        file = service.open(*added);
    }

    // Couldn't open the file for reading.
    if (!file)
    {
        LOG_err << "Failed to open file " << toNodeHandle(handle)
                << " for streaming: " << toString(file.error());

        return false;
    }

    auto f = dupUVFile(*file);
    if (!f)
    {
        return false;
    }
    httpctx->mCacheFile.init(std::move(f), static_cast<m_off_t>(offset));

    auto callback =
        [logName = httpctx->getLogName(), ctx = httpctx->weak_from_this(), offset, length](
            FileResultOr<FileStreamFDResult> result)
    {
        auto ctxPtr = ctx.lock();
        if (!ctxPtr)
        {
            LOG_warn << logName << "[Streaming] callback: httpctx has closed";
            return;
        }

        // This check is nice to have, as the uv loop can change it after checking
        if (ctxPtr->finished)
        {
            LOG_verbose << ctxPtr->logname << "[Streaming] callback: finished";
            return;
        }

        if (!result)
        {
            LOG_info << ctxPtr->logname
                     << "[Streaming] callback: stream return error: " << result.error();
            ctxPtr->failed = true;
            uv_async_send(&ctxPtr->asynchandle);
            return;
        }

        const auto receivedOffset = result->mOffset;
        const auto receivedLength = result->mLength;

        LOG_verbose << ctxPtr->logname << "[Streaming] callback: requested [" << offset << ","
                    << length << "] received [" << receivedOffset << "," << receivedLength << "]";

        // All requested has been delivered
        if (receivedLength == 0)
        {
            LOG_verbose << ctxPtr->logname << "[Streaming] callback: all delivered";
            return;
        }

        // More data available in the cached file
        if (const auto newAvailableBytes =
                ctxPtr->mCacheFile.onReceived(static_cast<m_off_t>(receivedOffset),
                                              static_cast<m_off_t>(receivedLength));
            !newAvailableBytes)
        {
            // Must be a bug
            assert(false);
            LOG_fatal << ctxPtr->logname << "[Streaming] callback: out of order read";
            ctxPtr->failed = true;
        }
        else
        {
            assert(*newAvailableBytes <= static_cast<m_off_t>(length));
        }

        // Notify the HTTP server
        uv_async_send(&ctxPtr->asynchandle);

        // Continue to read if there is no error
        if (!ctxPtr->failed)
            result->mContinue(receivedLength);
    };

    file_service::stream(FileStreamFDCallback{std::move(callback)},
                         std::move(*file),
                         offset,
                         length);
    return true;
}

int MegaHTTPServer::streamNode(MegaHTTPContext *httpctx)
{
    std::ostringstream response;
    MegaNode *node = httpctx->node;

    string name;
    const char *extension = NULL;
    const char *nodeName = httpctx->node->getName();
    if (nodeName)
    {
        name = nodeName;
    }

    string::size_type dotindex = name.find_last_of('.');
    if (dotindex != string::npos)
    {
        extension = name.c_str() + dotindex;
    }

    char *mimeType = MegaApi::getMimeType(extension);
    if (!mimeType)
    {
        mimeType = MegaApi::strdup("application/octet-stream");
    }

    m_off_t totalSize = node->getSize();
    m_off_t start = 0;
    m_off_t end = totalSize - 1;
    bool rangeRequested = false;
    if (httpctx->rangeStart >= 0)
    {
        start = httpctx->rangeStart;
        rangeRequested = true;
    }
    httpctx->rangeStart = start;

    if (httpctx->rangeEnd >= 0)
    {
        end = std::min(totalSize - 1, httpctx->rangeEnd);
    }
    httpctx->rangeEnd = end + 1;

    m_off_t len = end - start + 1;
    if (totalSize && (start < 0 || start >= totalSize || end < 0 || end >= totalSize || len <= 0 || len > totalSize) )
    {
        response << "HTTP/1.1 416 Requested Range Not Satisfiable\r\n"
            << "Content-Type: " << mimeType << "\r\n"
            << "Connection: close\r\n"
            << "Access-Control-Allow-Origin: *\r\n"
            << "Accept-Ranges: bytes\r\n"
            << "Content-Range: bytes 0-0/" << totalSize << "\r\n"
            << "\r\n";

        delete [] mimeType;
        httpctx->resultCode = 416;
        string resstr = response.str();
        sendHeaders(httpctx, &resstr);
        return 0;
    }

    if (rangeRequested)
    {
        response << "HTTP/1.1 206 Partial Content\r\n";
        response << "Content-Range: bytes " << start << "-" << end << "/" << totalSize << "\r\n";
    }
    else
    {
        response << "HTTP/1.1 200 OK\r\n";
    }

    response << "Content-Type: " << mimeType << "\r\n"
        << "Connection: close\r\n"
        << "Content-Length: " << len << "\r\n"
        << "Access-Control-Allow-Origin: *\r\n"
        << "Accept-Ranges: bytes\r\n"
        << "\r\n";

    delete[] mimeType;
    httpctx->lastBuffer = NULL;
    httpctx->lastBufferLen = 0;

    httpctx->streamingBuffer.setFileSize(totalSize);
    httpctx->streamingBuffer.setDuration(httpctx->node->getDuration());

    string resstr = response.str();
    if (httpctx->parser.method != HTTP_HEAD)
    {
        // Body data can be written to streamingBuffer before the header is sent
        httpctx->streamingBuffer.init(static_cast<size_t>(len) + resstr.size());
        httpctx->server->setMaxBufferSize(
            static_cast<int>(httpctx->streamingBuffer.getMaxBufferSize()));
        httpctx->server->setMaxOutputSize(
            static_cast<int>(httpctx->streamingBuffer.getMaxOutputSize()));
        httpctx->size = len;
    }

    sendHeaders(httpctx, &resstr);
    if (httpctx->parser.method == HTTP_HEAD)
    {
        return 0;
    }

    LOG_debug << httpctx->getLogName() << "Requesting range. From " << start << "  size " << len;
    httpctx->rangeWritten = 0;
    if (start || len)
    {
        httpctx->streamingBuffer.reset(!httpctx->lastBufferLen, resstr.size());
        if (!startStream(httpctx, static_cast<uint64_t>(start), static_cast<uint64_t>(len)))
        {
            LOG_err << httpctx->getLogName() << "Finishing due to an error of startStream";
            closeConnection(httpctx);
        }
    }
    else
    {
        MegaHTTPServer *httpserver = ((MegaHTTPServer *)httpctx->server);
        LOG_debug << httpctx->getLogName() << "Skipping startStreaming call since empty file";
        httpserver->processWriteFinished(httpctx, 0);
    }
    return 0;
}

void MegaHTTPServer::sendHeaders(MegaHTTPContext *httpctx, string *headers)
{
    LOG_debug << httpctx->getLogName() << "Response headers: " << *headers;
    httpctx->streamingBuffer.append(headers->data(), headers->size());
    uv_buf_t resbuf = httpctx->streamingBuffer.nextBuffer();
    httpctx->size += headers->size();
    httpctx->lastBuffer = resbuf.base;
    httpctx->lastBufferLen = resbuf.len;

#ifdef ENABLE_EVT_TLS
    if (httpctx->server->useTLS)
    {
        assert (resbuf.len);
        int err = evt_tls_write(httpctx->evt_tls, resbuf.base, resbuf.len, onWriteFinished_tls);
        if (err <= 0)
        {
            LOG_warn << httpctx->getLogName()
                     << "Finishing due to an error sending the response: " << err;
            closeConnection(httpctx);
        }
    }
    else
    {
#endif
        uv_write_t *req = new uv_write_t();
        req->data = httpctx;
        if (int err = uv_write(req, (uv_stream_t*)&httpctx->tcphandle, &resbuf, 1, onWriteFinished))
        {
            delete req;
            LOG_warn << httpctx->getLogName()
                     << "Finishing due to an error sending the response: " << err;
            closeTCPConnection(httpctx);
        }
#ifdef ENABLE_EVT_TLS
    }
#endif
}

void MegaHTTPServer::processAsyncEvent(MegaTCPContext* tcpctx)
{
    MegaHTTPContext* httpctx = dynamic_cast<MegaHTTPContext *>(tcpctx);

    if (httpctx->finished)
    {
        LOG_debug << httpctx->getLogName() << "HTTP link closed, ignoring async event";
        return;
    }

    if (httpctx->failed)
    {
        LOG_warn << httpctx->getLogName() << "Streaming transfer failed. Closing connection.";
        closeConnection(httpctx);
        return;
    }

    uv_mutex_lock(&httpctx->mutex_responses);
    while (httpctx->responses.size())
    {
        sendHeaders(httpctx,&httpctx->responses.front());
        httpctx->responses.pop_front();
    }
    uv_mutex_unlock(&httpctx->mutex_responses);

    if (httpctx->nodereceived)
    {
        httpctx->nodereceived = false;
        if (!httpctx->node || httpctx->nodename != httpctx->node->getName())
        {
            if (!httpctx->node)
            {
                LOG_warn << httpctx->getLogName() << "Public link not found";
            }
            else
            {
                LOG_warn << httpctx->getLogName() << "Invalid name for public link";
            }

            httpctx->resultCode = 404;
            string resstr = "HTTP/1.1 404 Not Found\r\nConnection: close\r\n\r\n";
            sendHeaders(httpctx, &resstr);
            return;
        }

        streamNode(httpctx);
        return;
    }

    sendNextBytes(httpctx);
}

// Adaptive rate limiting at TCP output — only delay when data is delivered
// faster than the throttle rate. Uses one-shot uv_timer to avoid blocking the event loop.
// return false if rate limit is not needed, otherwise true.
static bool rateLimiting(MegaHTTPContext& httpctx)
{
    using std::chrono::duration_cast;
    using std::chrono::milliseconds;
    using std::chrono::steady_clock;

    const auto throttleBps = httpctx.megaApi->httpServerGetThrottleBitrate();

    // Throttle Bps may changes for the following reasons:
    // 1. a new playback
    // 2. playback speed change such as from 1x to 2x
    if (throttleBps != httpctx.throttleBps)
    {
        // Set to new
        httpctx.throttleBps = throttleBps;

        // Reset
        httpctx.throttleLastChunkTime = std::nullopt;

        // Remeber offset in this throttleBps started cycle
        if (httpctx.rangeWritten > 0)
        {
            httpctx.throttleOffset = httpctx.rangeWritten;
        }
    }

    // Disabled
    if (throttleBps <= 0)
        return false;

    // Record start time on first throttled entry for this connection
    if (!httpctx.throttleLastChunkTime)
    {
        httpctx.throttleLastChunkTime = steady_clock::now();
    }

    // No more data written yet
    if (httpctx.rangeWritten <= 0 || httpctx.rangeWritten <= httpctx.throttleOffset)
        return false;

    const auto throttleWritten = httpctx.rangeWritten - httpctx.throttleOffset.value_or(0);
    const auto now = steady_clock::now();
    const int64_t bytesPerSec = static_cast<int64_t>(throttleBps) / 8;
    const int64_t elapsedMs =
        duration_cast<milliseconds>(now - *httpctx.throttleLastChunkTime).count();
    const int64_t expectedMs = static_cast<int64_t>(throttleWritten * 1000 / bytesPerSec);
    const int64_t delayMs = expectedMs - elapsedMs;

    if (delayMs <= 1)
        return false;

    HTTP_verbose_timed << httpctx.getLogName() << "[Throttle] TCP delay " << delayMs
                       << "ms (expected=" << expectedMs << "ms, elapsed=" << elapsedMs
                       << "ms, written=" << throttleWritten << ", throttle=" << throttleBps
                       << " bps)";

    auto timerHandler = [](uv_timer_t* t)
    {
        auto* weakHandle = static_cast<std::weak_ptr<MegaHTTPContext>*>(t->data);

        uv_timer_stop(t);

        uv_close(reinterpret_cast<uv_handle_t*>(t),
                 [](uv_handle_t* h)
                 {
                     delete reinterpret_cast<uv_timer_t*>(h);
                 });

        if (!weakHandle)
            return;

        auto ctx = weakHandle->lock();
        if (!ctx || ctx->finished)
            return;

        uv_async_send(&ctx->asynchandle);
    };

    // Init a timer
    auto* timer = new uv_timer_t();
    uv_timer_init(httpctx.server->getUvLoop(), timer);
    timer->data = new std::weak_ptr<MegaHTTPContext>{httpctx.weak_from_this()};

    // start the timer and timer's ownership belongs to the timerHandler
    uv_timer_start(timer, timerHandler, static_cast<uint64_t>(delayMs), 0);

    return true;
}

void MegaHTTPServer::sendNextBytes(MegaHTTPContext *httpctx)
{
    if (httpctx->finished)
    {
        LOG_debug << httpctx->getLogName() << "HTTP link closed, aborting write";
        return;
    }

    if (httpctx->lastBuffer)
    {
        LOG_verbose << httpctx->getLogName()
                    << "[Streaming] Skipping write due to another ongoing write";
        return;
    }

    if (rateLimiting(*httpctx))
        return;

    uv_mutex_lock(&httpctx->mutex);
    if (httpctx->lastBufferLen)
    {
        httpctx->streamingBuffer.freeData(httpctx->lastBufferLen);
        httpctx->lastBufferLen = 0;
    }

    if (httpctx->tcphandle.write_queue_size > httpctx->streamingBuffer.availableCapacity() / 8)
    {
        LOG_warn << httpctx->getLogName() << "[Streaming] Skipping write. Too much queued data. "
                 << httpctx->streamingBuffer.bufferStatus();
        uv_mutex_unlock(&httpctx->mutex);
        return;
    }

    uv_buf_t resbuf = httpctx->streamingBuffer.nextBuffer();

    uv_mutex_unlock(&httpctx->mutex);

    if (!resbuf.len)
    {
        LOG_debug << httpctx->getLogName() << "[Streaming] Skipping write. No data available. "
                  << httpctx->streamingBuffer.bufferStatus();
        // Read more from fd to stream buffer
        readCacheFile(httpctx);
        return;
    }

    httpctx->rangeWritten += resbuf.len;
    httpctx->lastBuffer = resbuf.base;
    httpctx->lastBufferLen = resbuf.len;

#ifdef ENABLE_EVT_TLS
    if (httpctx->server->useTLS)
    {
        // notice this, contrary to !useTLS is synchronous
        int err = evt_tls_write(httpctx->evt_tls, resbuf.base, resbuf.len, onWriteFinished_tls);
        if (err <= 0)
        {
            LOG_warn << httpctx->getLogName()
                     << "[Streaming] Finishing due to an error sending the response: " << err;
            evt_tls_close(httpctx->evt_tls, on_evt_tls_close);
        }
    }
    else
    {
#endif
        uv_write_t* req = new uv_write_t();
        req->data = httpctx;

        if (uv_write(req, (uv_stream_t*)&httpctx->tcphandle, &resbuf, 1, onWriteFinished))
        {
            delete req;
            httpctx->finished = true;
            if (!uv_is_closing((uv_handle_t*)&httpctx->tcphandle))
            {
                uv_close((uv_handle_t*)&httpctx->tcphandle, onClose);
            }
        }
#ifdef ENABLE_EVT_TLS
    }
#endif
}

std::atomic_uint32_t MegaHTTPContext::nextId{0u};

// Decide by the number of parallel public file link video streaming likely to be.
// 16 might be enough
constexpr std::size_t MAX_PUBLIC_NODES_CAPACITY = 16;

PublicNodeCache MegaHTTPContext::publicNodes{MAX_PUBLIC_NODES_CAPACITY};

PublicNodeCache::PublicNodeCache(std::size_t capacity):
    mNodes{capacity}
{}

unique_ptr<MegaNode> PublicNodeCache::getCopy(handle h)
{
    const std::lock_guard l{mMutex};
    if (const std::optional<shared_ptr<MegaNode>> ptr = mNodes.get(h); ptr && *ptr)
    {
        return unique_ptr<MegaNode>{(*ptr)->copy()};
    }

    return nullptr;
}

void PublicNodeCache::put(handle h, const std::shared_ptr<MegaNode> ptr)
{
    const std::lock_guard l{mMutex};
    mNodes.put(h, std::move(ptr));
}

m_off_t MegaHTTPContext::CacheFile::availableBytes() const
{
    return mAvailableBytes;
}

m_off_t MegaHTTPContext::CacheFile::consumedBytes() const
{
    return mConsumedBytes;
}

uv_file MegaHTTPContext::CacheFile::fd() const
{
    return mFd.get();
}

void MegaHTTPContext::CacheFile::init(AutoUVFile&& fd, m_off_t offset)
{
    assert(fd);

    mAvailableBytes = 0;
    mConsumedBytes = 0;
    mFd = std::move(fd);
    mReading = false;
    mOffset = offset;
}

m_off_t MegaHTTPContext::CacheFile::offset() const
{
    return mOffset;
}

void MegaHTTPContext::CacheFile::addConsumedBytes(m_off_t delta)
{
    mConsumedBytes += delta;
}

std::optional<m_off_t> MegaHTTPContext::CacheFile::onReceived(m_off_t receivedOffset,
                                                              m_off_t receivedLength)
{
    if (receivedOffset != mOffset + mAvailableBytes)
    {
        return std::nullopt;
    }

    // The file service delivers callbacks sequentially, so mAvailableBytes has no concurrent
    // writers.
    mAvailableBytes += receivedLength;
    return mAvailableBytes;
}

bool MegaHTTPContext::CacheFile::isReading() const
{
    return mReading;
}

void MegaHTTPContext::CacheFile::setReading(bool value)
{
    mReading = value;
}

MegaHTTPContext::MegaHTTPContext():
    contextId{nextId++},
    logname{"(HttpCtx#" + std::to_string(contextId) + ") "},
    streamingBuffer(logname)
{
    rangeStart = -1;
    rangeEnd = -1;
    rangeWritten = -1;
    range = false;
    failed = false;
    nodereceived = false;
    resultCode = API_EINTERNAL;
    node = NULL;
    nodesize = -1;
    messageBody = NULL;
    messageBodySize = 0;
    tmpFileAccess = NULL;
    newParentNode = UNDEF;
    nodeToMove = UNDEF;
    depth = -1;
    overwrite = true; //GVFS-DAV via command line does not include this header (assumed true)
    lastBuffer = NULL;
    lastBufferLen = 0;

    // Mutex to protect the data buffer
    uv_mutex_init(&mutex_responses);
}

MegaHTTPContext::~MegaHTTPContext()
{
    delete node;
    if (tmpFileName.size())
    {
        LocalPath localPath = LocalPath::fromAbsolutePath(tmpFileName);
        server->fsAccess->unlinklocal(localPath);
    }
    delete [] messageBody;
    uv_mutex_destroy(&mutex_responses);
}

void MegaHTTPContext::onTransferStart(MegaApi*, [[maybe_unused]] MegaTransfer* transfer)
{
    assert(transfer->getType() == MegaTransfer::TYPE_UPLOAD);
}

bool MegaHTTPContext::onTransferData(MegaApi*, MegaTransfer*, char*, size_t)
{
    // Never be called
    assert(false);
    return false;
}

void MegaHTTPContext::onTransferFinish(MegaApi *, MegaTransfer *, MegaError *e)
{
    if (finished)
    {
        LOG_debug << logname << "HTTP link closed, ignoring the result of the transfer";
        return;
    }

    MegaHTTPServer* httpserver = dynamic_cast<MegaHTTPServer *>(server);

    int ecode = e->getErrorCode();

    if (parser.method == HTTP_PUT)
    {
        if (ecode == API_OK)
        {
            httpserver->returnHttpCodeAsync(this, 201); //TODO actually if resource already existed this should be 200
        }
        else
        {
            httpserver->returnHttpCodeAsyncBasedOnRequestError(this, e);
        }
    }

    if (ecode != API_OK && ecode != API_EINCOMPLETE)
    {
        LOG_warn << logname << "Transfer failed with error code: " << ecode;
        failed = true;
    }
    uv_async_send(&asynchandle);
}

void MegaHTTPContext::onRequestFinish(MegaApi *, MegaRequest *request, MegaError *e)
{
    if (finished)
    {
        LOG_debug << logname << "HTTP link closed, ignoring the result of the request";
        return;
    }
    MegaHTTPServer* httpserver = dynamic_cast<MegaHTTPServer *>(server);

    if (request->getType() == MegaRequest::TYPE_MOVE)
    {
        if (e->getErrorCode() == MegaError::API_OK)
        {
            if (this->newname.size())
            {
                MegaNode *nodetoRename = this->megaApi->getNodeByHandle(request->getNodeHandle());
                if (!nodetoRename || !strcmp(nodetoRename->getName(), newname.c_str()))
                {
                    httpserver->returnHttpCodeAsync(this, 204);
                }
                else
                {
                    this->megaApi->renameNode(nodetoRename, newname.c_str(), this);
                }
                delete nodetoRename;
            }
            else
            {
                httpserver->returnHttpCodeAsync(this, 204);
            }
        }
        else
        {
            httpserver->returnHttpCodeAsyncBasedOnRequestError(this, e);
        }
    }
    else if (request->getType() == MegaRequest::TYPE_RENAME)
    {
        if (e->getErrorCode() == MegaError::API_OK )
        {
            httpserver->returnHttpCodeAsync(this, 204);
        }
        else
        {
            httpserver->returnHttpCodeAsyncBasedOnRequestError(this, e);
        }
    }
    else if (request->getType() == MegaRequest::TYPE_REMOVE)
    {
        if (e->getErrorCode() == MegaError::API_OK)
        {
            MegaNode *n = this->megaApi->getNodeByHandle(nodeToMove);
            MegaNode *p = this->megaApi->getNodeByHandle(newParentNode);
            if (n && p) //delete + move
            {
                this->megaApi->moveNode(n, p, this);
            }
            else
            {
                httpserver->returnHttpCodeAsync(this, 204); // Standard success response
            }

            nodeToMove = UNDEF;
            newParentNode = UNDEF;
            delete n;
            delete p;
        }
        else
        {
            httpserver->returnHttpCodeAsyncBasedOnRequestError(this, e);
        }
    }
    else if (request->getType() == MegaRequest::TYPE_CREATE_FOLDER)
    {
        if (e->getErrorCode() == MegaError::API_OK)
        {
            httpserver->returnHttpCodeAsync(this, 201);
        }
        else
        {
            httpserver->returnHttpCodeAsyncBasedOnRequestError(this, e);
        }
    }
    else if (request->getType() == MegaRequest::TYPE_COPY)
    {
        if (e->getErrorCode() == MegaError::API_OK)
        {
            httpserver->returnHttpCodeAsync(this, 201);
        }
        else
        {
            httpserver->returnHttpCodeAsyncBasedOnRequestError(this, e);
        }
    }
    else if (request->getType() == MegaRequest::TYPE_GET_PUBLIC_NODE)
    {
        node = request->getPublicMegaNode();
        nodereceived = true;
        if (node)
        {
            publicNodes.put(node->getHandle(), shared_ptr<MegaNode>{node->copy()});
        }
    }
    uv_async_send(&asynchandle);
}


//////////////////////////////
//  MegaFTPServer specifics //
//////////////////////////////

/**
 * Gets permissions string: e.g: 777 -> rwxrwxrwx
 * @param permissions permissions
 * @param permsString permission string buffer
 */
void MegaFTPServer::getPermissionsString(int permissions, char *permsString)
{
    string ps = "";
    for(int i = 0; i<3; i++) // user, group, others
    {
        int curperm = permissions%10;
        permissions = permissions / 10;

        bool read = (curperm >> 2) & 0x1;
        bool write = (curperm >> 1) & 0x1;
        bool exec = (curperm >> 0) & 0x1;

        char rwx[4];
        snprintf(rwx, sizeof(rwx), "%c%c%c",read?'r':'-' ,write?'w':'-', exec?'x':'-');
        rwx[3]='\0';
        ps = rwx + ps;
    }
    strncat(permsString, ps.c_str(), ps.size() + 1);
}

//ftp_parser_settings MegaTCPServer::parsercfg;
MegaFTPServer::MegaFTPServer(MegaApiImpl *megaApi, string basePath, int dataportBegin, int dataPortEnd, bool useTLS, string certificatepath, string keypath)
: MegaTCPServer(megaApi, basePath, useTLS, certificatepath, keypath)
{
    nodeHandleToRename = UNDEF;
    this->pport = dataportBegin;
    this->dataportBegin = dataportBegin;
    this->dataPortEnd = dataPortEnd;

    crlfout = "\r\n";
}

MegaFTPServer::~MegaFTPServer()
{
    // if not stopped, the uv thread might want to access a pointer to this.
    // though this is done in the parent destructor, it could try to access it after vtable has been erased
    stop();
}

MegaTCPContextPtr MegaFTPServer::initializeContext(uv_stream_t* server_handle)
{
    shared_ptr<MegaFTPContext> ftpctx = std::make_shared<MegaFTPContext>();

    // Set connection data
    MegaFTPServer* ftpServer = (MegaFTPServer*)(server_handle->data);
    ftpctx->server = ftpServer;
    ftpctx->megaApi = ftpServer->megaApi;
    ftpctx->tcphandle.data = ftpctx.get();
    ftpctx->asynchandle.data = ftpctx.get();

    return ftpctx;
}

void MegaFTPServer::processWriteFinished(MegaTCPContext*, int status)
{
    LOG_verbose << "MegaFTPServer::processWriteFinished. status=" << status;
}

string MegaFTPServer::getListingLineFromNode(MegaNode *child, string nameToShow)
{
    char perms[10];
    memset(perms,0,10);
    //str_perm((statbuf.st_mode & ALLPERMS), perms);
    getPermissionsString(child->isFolder() ? 777 : 664, perms);

    char timebuff[80];
    time_t rawtime = (child->isFolder()?child->getCreationTime():child->getModificationTime());
    struct tm time;
    m_localtime(rawtime, &time);
    strftime(timebuff,80,"%b %d %H:%M",&time);

    char toprint[3000];
    snprintf(toprint, sizeof(toprint),
            "%c%s %5d %4d %4d %8"
            PRId64
            " %s %s",
            (child->isFolder())?'d':'-',
            perms,
            1,//number of contents for folders
            1000, //uid
            1000, //gid
             (child->isFolder())?4:child->getSize(),
            timebuff,
            nameToShow.size()?nameToShow.c_str():child->getName());

    return toprint;
}


string MegaFTPServer::getFTPErrorString(int errorcode, string argument)
{
    switch (errorcode)
    {
    case 110:
        return "Restart marker reply.";
    case 120:
        return "Service ready in " + argument + " minutes.";
    case 125:
        return "Data connection already open; transfer starting.";
    case 150:
        return "File status okay; about to open data connection.";
    case 200:
        return "Command okay.";
    case 202:
        return "Command not implemented, superfluous at this site.";
    case 211:
        return "System status, or system help reply.";
    case 212:
        return "Directory status.";
    case 213:
        return "File status.";
    case 214:
        return "Help message.";
    case 215:
        return "NAME system type.";
    case 220:
        return "Service ready for new user.";
    case 221:
        return "Service closing control connection.";
    case 225:
        return "Data connection open; no transfer in progress.";
    case 226:
        return "Closing data connection. Requested file action successful.";
    case 227:
        return "Entering Passive Mode (h1,h2,h3,h4,p1,p2).";
    case 230:
        return "User logged in, proceed.";
    case 250:
        return "Requested file action okay, completed.";
    case 257:
        return argument + " created.";
    case 331:
        return "User name okay, need password.";
    case 332:
        return "Need account for login.";
    case 350:
        return "Requested file action pending further information.";
    case 421:
        return "Service not available, closing control connection.";
    case 425:
        return "Can't open data connection.";
    case 426:
        return "Connection closed; transfer aborted.";
    case 450:
        return "Requested file action not taken. File unavailable (e.g., file busy).";
    case 451:
        return "Requested action aborted: local error in processing.";
    case 452:
        return "Requested action not taken. Insufficient storage space in system.";
    case 500:
        return "Syntax error, command unrecognized.";
    case 501:
        return "Syntax error in parameters or arguments.";
    case 502:
        return "Command not implemented.";
    case 503:
        return "Bad sequence of commands.";
    case 504:
        return "Command not implemented for that parameter.";
    case 530:
        return "Not logged in.";
    case 532:
        return "Need account for storing files.";
    case 550:
        return "Requested action not taken. File unavailable (e.g., file not found, no access).";
    case 551:
        return "Requested action aborted: page type unknown.";
    case 552:
        return "Requested file action aborted. Exceeded storage allocation."; // (for current directory or dataset).
    case 553:
        return "Requested action not taken. File name not allowed.";
    default:
        return "Unknown Error";
    }
}

void MegaFTPServer::returnFtpCodeBasedOnRequestError(MegaFTPContext* ftpctx, MegaError *e)
{
    int reqError = e->getErrorCode();
    int ftpreturncode = 500;

    switch(reqError)
    {
    case API_OK:
        ftpreturncode = 300;
        break;
    case API_EACCESS:
        ftpreturncode = 550; //this might not be accurate
        break;
    case API_EOVERQUOTA:
    case API_EGOINGOVERQUOTA:
        ftpreturncode = 452; // 552?
        break;
    case API_EAGAIN:
    case API_ERATELIMIT:
    case API_ETEMPUNAVAIL:
        ftpreturncode = 120;
        break;
    case API_EREAD:
        ftpreturncode = 450;
        break;
    case API_ECIRCULAR:
        ftpreturncode = 508;
        break;
    default:
        ftpreturncode = 503;
        break;
    }

    LOG_debug << "FTP petition failed. request error = " << reqError << " FTP status to return = " << ftpreturncode;
    string errorMessage = e->getErrorString(reqError);
    return returnFtpCode(ftpctx, ftpreturncode, errorMessage);
}

void MegaFTPServer::returnFtpCode(MegaFTPContext* ftpctx, int errorCode, string errorMessage)
{
    MegaFTPServer* ftpserver = dynamic_cast<MegaFTPServer *>(ftpctx->server);

    std::ostringstream response;
    response << errorCode << " " << (errorMessage.size() ? errorMessage : getFTPErrorString(errorCode))
             << ftpserver->crlfout;

    string resstr = response.str();
    uv_mutex_lock(&ftpctx->mutex_responses);
    ftpctx->responses.push_back(resstr);
    uv_mutex_unlock(&ftpctx->mutex_responses);
    uv_async_send(&ftpctx->asynchandle);
}

void MegaFTPServer::returnFtpCodeAsyncBasedOnRequestError(MegaFTPContext* ftpctx, MegaError *e)
{
    return returnFtpCodeBasedOnRequestError(ftpctx, e);
}

void MegaFTPServer::returnFtpCodeAsync(MegaFTPContext* ftpctx, int errorCode, string errorMessage)
{
    return returnFtpCode(ftpctx, errorCode, errorMessage);
}

// you get the ownership
MegaNode *MegaFTPServer::getBaseFolderNode(string path)
{
    if (path.size() && path.at(0) == '/')
    {
        string rest = path.substr(1);
        size_t possep = rest.find('/');
        handle h = megaApi->base64ToHandle(rest.substr(0,possep).c_str());
        MegaNode *n = megaApi->getNodeByHandle(h);
        if (possep != string::npos && possep != (rest.size() - 1) )
        {
            if (n)
            {
                if (rest.size() > (possep + 1))
                {
                    rest = rest.substr(possep + 1);
                    if (rest == n->getName())
                    {
                        return n;
                    }
                    if (rest.size() > strlen(n->getName()) && (rest.at(strlen(n->getName())) == '/' ) && (rest.find(n->getName()) == 0) )
                    {
                        return n;
                    }
                }
                delete n;
            }
        }
        else
        {
            return n;
        }
    }
    return NULL;
}

// you get the ownership
MegaNode *MegaFTPServer::getNodeByFullFtpPath(string path)
{
    if (path.size() && path.at(0) == '/')
    {
        string rest = path.substr(1);
        size_t possep = rest.find('/');
        handle h = megaApi->base64ToHandle(rest.substr(0,possep).c_str());
        MegaNode *n = megaApi->getNodeByHandle(h);
        if (possep != string::npos && possep != (rest.size() - 1) )
        {
            if (n)
            {
                if (rest.size() > possep)
                {
                    rest = rest.substr(possep + 1);
                    if (rest == n->getName())
                    {
                        return n;
                    }
                    if (rest.size() > strlen(n->getName()) && (rest.at(strlen(n->getName())) == '/' ) && (rest.find(n->getName()) == 0) )
                    {
                        string relpath = rest.substr(strlen(n->getName())+1);
                        MegaNode *toret = megaApi->getNodeByPath(relpath.c_str(), n);
                        delete n;
                        return toret;
                    }
                }
                delete n;
            }
        }
        else
        {
            return n;
        }
    }
    return NULL;
}

MegaNode * MegaFTPServer::getNodeByFtpPath(MegaFTPContext* ftpctx, string path)
{
    if (ftpctx->atroot && path.size() && path.at(0) != '/')
    {
        path= "/" + path;
    }
    else if (ftpctx->athandle && path.size() && path.at(0) != '/')
    {
        char *cshandle = ftpctx->megaApi->handleToBase64(ftpctx->cwd);
        string handle(cshandle);
        delete []cshandle;

        path= "/" + handle + "/" + path;
    }
    else if (path.size() && path.at(0) != '/')
    {
        path = ftpctx->cwdpath + "/" + path;
        path = shortenpath(path);
    }

    if (path.find("..") == 0)
    {
        string fullpath = ftpctx->cwdpath + "/" + path;
        size_t seppos = fullpath.find("/");
        int count = 0;
        while (seppos != string::npos && fullpath.size() > (seppos +1) )
        {
            string part = fullpath.substr(0,seppos);
            if (part.size() && part != "..")
            {
                count++;
            }
            if (part == "..")
            {
                count--;
                if (count < 2)
                {
                    return NULL; // do not allow to escalate in the path
                }
            }

            fullpath = fullpath.substr(seppos+1);
            if (fullpath == ".." && count == 2)
            {
                return NULL; // do not allow to escalate in the path
            }
            seppos = fullpath.find("/");
        }
    }

    if (path.size() && path.at(0) == '/')
    {
        MegaNode *baseFolderNode = getBaseFolderNode(path);
        if (!baseFolderNode)
        {
            return NULL;
        }
        if (!isHandleAllowed(baseFolderNode->getHandle()) )
        {
            delete baseFolderNode;
            return NULL;
        }
        delete baseFolderNode;

        return getNodeByFullFtpPath(path);
    }
    else //it should only enter here if path == ""
    {
        MegaNode *n = ftpctx->megaApi->getNodeByHandle(ftpctx->cwd);
        if (!n)
        {
            return NULL;
        }
        MegaNode *toret = ftpctx->megaApi->getNodeByPath(path.c_str(), n);
        delete n;
        return toret;
    }
}

std::string MegaFTPServer::shortenpath(std::string path)
{
    string orig = path;

    while ((path.size() > 1) && path.at(path.size() - 1) == '/') // remove trailing /
    {
        path = path.substr(0,path.size()-1);
    }
    list<string> parts;
    size_t seppos = path.find("/");
    while (seppos != string::npos && path.size() > (seppos +1) )
    {
        string part = path.substr(0,seppos);
        if (part.size() && part != "..")
        {
            parts.push_back(part);
        }
        if (part == "..")
        {
            if (!parts.size())
            {
                return "INVALIDPATH"; // FAILURE!
            }
            parts.pop_back();
        }

        path = path.substr(seppos+1);
        if (path == "..")
        {
            if (!parts.size())
            {
                return "INVALIDPATH"; // FAILURE!
            }
            parts.pop_back();
            path = "";
        }
        seppos = path.find("/");
    }
    if (path.size() && path != "..")
    {
        parts.push_back(path);
    }

    string toret;
    if (!parts.size() && orig.size() && orig.at(0) == '/')
    {
        toret = "/";
    }
    else
    {
        while (parts.size())
        {
            toret.append("/");
            toret.append(parts.front());
            parts.pop_front();
        }
    }
    return toret;
}

std::string MegaFTPServer::cdup(handle parentHandle, MegaFTPContext* ftpctx)
{
    string response;
    MegaNode *newcwd = ftpctx->megaApi->getNodeByHandle(parentHandle);
    if (newcwd)
    {
        bool allowed = isHandleAllowed(newcwd->getHandle()) || isHandleAllowed(newcwd->getParentHandle());
        MegaNode *pn = ftpctx->megaApi->getNodeByHandle(newcwd->getHandle());
        while (!allowed && pn)
        {
            MegaNode *aux = pn;
            pn = ftpctx->megaApi->getNodeByHandle(pn->getParentHandle());
            delete aux;
            if (pn)
            {
                allowed = isHandleAllowed(pn->getParentHandle());
            }
        }
        delete pn;

        if (!allowed)
        {
            LOG_warn << "Ftp client trying to access not allowed path";
            response = "550 Path not allowed";
        }
        else if (newcwd->isFolder() && newcwd->getHandle() != UNDEF)
        {
            ftpctx->cwd = newcwd->getHandle();
            ftpctx->cwdpath = ftpctx->cwdpath + "/..";
            ftpctx->cwdpath = shortenpath(ftpctx->cwdpath);
            ftpctx->athandle = false;
            ftpctx->atroot = false;
            ptrdiff_t seps = std::count(ftpctx->cwdpath.begin(), ftpctx->cwdpath.end(), '/');
            if (seps < 2)
            {
                ftpctx->cwdpath = string("/") + megaApi->handleToBase64(newcwd->getHandle()) + "/" + newcwd->getName();
            }
            ftpctx->parentcwd = newcwd->getParentHandle();

            response = "250 Directory successfully changed";
        }
        else
        {
            response = "550 CDUP failed.";
        }

        delete newcwd;
    }
    else
    {
        response = "550 Not Found";
    }
    return response;
}

std::string MegaFTPServer::cd(string newpath, MegaFTPContext* ftpctx)
{
    string response;
    if (newpath == "/")
    {
        MegaNode *rootNode = megaApi->getRootNode();
        if (rootNode)
        {
            ftpctx->cwd = rootNode->getHandle();
            ftpctx->cwdpath = "/";
            ftpctx->atroot = true;
            ftpctx->athandle = false;
            response = "250 Directory successfully changed";
            delete rootNode;
            return response;
        }
        response = "550 CWD not Found.";
        return response;
    }

    MegaNode *newcwd = getNodeByFtpPath(ftpctx, newpath);
    if (!newcwd)
    {
        response = "550 CWD not Found.";
        return response;
    }

    ftpctx->cwd = newcwd->getHandle();
    if (newpath.size() && newpath.at(0) == '/')
    {
        ftpctx->cwdpath = newpath;
    }
    else // relative paths!
    {
        ftpctx->cwdpath = (ftpctx->cwdpath == "/"?"":ftpctx->cwdpath) + "/" + newpath;
    }
    ftpctx->cwdpath = shortenpath(ftpctx->cwdpath);
    ftpctx->athandle = false;
    string handlepath = "/";

    char *cshandle = megaApi->handleToBase64(newcwd->getHandle());
    string shandle(cshandle);
    delete []cshandle;

    handlepath.append(shandle);
    if (ftpctx->cwdpath == handlepath || ftpctx->cwdpath == shandle || ftpctx->cwdpath == (handlepath +"/") )
    {
        ftpctx->cwdpath = handlepath;
        ftpctx->athandle = true;
    }
    ftpctx->atroot = false;
    ftpctx->parentcwd = newcwd->getParentHandle();

    if (ftpctx->athandle || newcwd->isFolder())
    {
        response = "250 Directory successfully changed";
    }
    else
    {
        response = "550 CWD failed."; //chrome requires this
    }
    delete newcwd;
    return response;
}

void MegaFTPServer::processReceivedData(MegaTCPContext *tcpctx, ssize_t nread, const uv_buf_t * buf)
{
    MegaFTPContext* ftpctx = dynamic_cast<MegaFTPContext *>(tcpctx);

    ssize_t parsed = -1; ((void)(parsed));
    string petition;
    std::string command;
    string response;
    bool delayresponse = false;

    if (!nread)
    {
        LOG_debug << " Discarding processReceivedData read = " << nread;
        return;
    }

    uv_mutex_lock(&tcpctx->mutex);

    if (nread >= 0)
    {
        bool failed = false;
        const char *separators = " ";
        const char *crlf = "\r\n";

        petition = string(buf->base, static_cast<size_t>(nread));

        LOG_verbose << "FTP Server received: " << petition << " at port = " << port;

        size_t psep = petition.find_first_of(separators);
        size_t psepend = petition.find(crlf);

        if (psepend != petition.size()-strlen(crlf))
        {
            parsed = -1;
            failed = true;

            LOG_warn << " Failed to parse petition:<" << petition << ">" << " psep=" << psep << " psepend=" << psepend
                     << " petition.size=" << petition.size() << " tcpctx=" << tcpctx;
        }
        else
        {
            parsed = static_cast<ssize_t>(petition.size());
            petition = petition.substr(0,psepend);
            command = petition.substr(0,psep);
            for (char& c : command) { c = static_cast<char>(toupper(c)); };
        }

        if (failed)
        {
            ftpctx->command = FTP_CMD_INVALID;
        }
        else if(command == "USER")
        {
            ftpctx->command = FTP_CMD_USER;
        }
        else if(command == "PASS")
        {
            ftpctx->command = FTP_CMD_PASS;
        }
        else if(command == "ACCT")
        {
            ftpctx->command = FTP_CMD_ACCT;
        }
        else if(command == "CWD")
        {
            ftpctx->command = FTP_CMD_CWD;
        }
        else if(command == "CDUP")
        {
            ftpctx->command = FTP_CMD_CDUP;
        }
        else if(command == "SMNT")
        {
            ftpctx->command = FTP_CMD_SMNT;
        }
        else if(command == "QUIT")
        {
            ftpctx->command = FTP_CMD_QUIT;
        }
        else if(command == "REIN")
        {
            ftpctx->command = FTP_CMD_REIN;
        }
        else if(command == "PORT")
        {
            ftpctx->command = FTP_CMD_PORT;
        }
        else if(command == "PASV")
        {
            ftpctx->command = FTP_CMD_PASV;
        }
        else if(command == "TYPE")
        {
            ftpctx->command = FTP_CMD_TYPE;
        }
        else if(command == "STRU")
        {
            ftpctx->command = FTP_CMD_STRU;
        }
        else if(command == "MODE")
        {
            ftpctx->command = FTP_CMD_MODE;
        }
        else if(command == "RETR")
        {
            ftpctx->command = FTP_CMD_RETR;
        }
        else if(command == "STOR")
        {
            ftpctx->command = FTP_CMD_STOR;
        }
        else if(command == "STOU")
        {
            ftpctx->command = FTP_CMD_STOU;
        }
        else if(command == "APPE")
        {
            ftpctx->command = FTP_CMD_APPE;
        }
        else if(command == "ALLO")
        {
            ftpctx->command = FTP_CMD_ALLO;
        }
        else if(command == "REST")
        {
            ftpctx->command = FTP_CMD_REST;
        }
        else if(command == "RNFR")
        {
            ftpctx->command = FTP_CMD_RNFR;
        }
        else if(command == "RNTO")
        {
            ftpctx->command = FTP_CMD_RNTO;
        }
        else if(command == "ABOR")
        {
            ftpctx->command = FTP_CMD_ABOR;
        }
        else if(command == "DELE")
        {
            ftpctx->command = FTP_CMD_DELE;
        }
        else if(command == "RMD")
        {
            ftpctx->command = FTP_CMD_RMD;
        }
        else if(command == "MKD")
        {
            ftpctx->command = FTP_CMD_MKD;
        }
        else if(command == "PWD")
        {
            ftpctx->command = FTP_CMD_PWD;
        }
        else if(command == "LIST")
        {
            ftpctx->command = FTP_CMD_LIST;
        }
        else if(command == "NLST")
        {
            ftpctx->command = FTP_CMD_NLST;
        }
        else if(command == "SITE")
        {
            ftpctx->command = FTP_CMD_SITE;
        }
        else if(command == "SYST")
        {
            ftpctx->command = FTP_CMD_SYST;
        }
        else if(command == "STAT")
        {
            ftpctx->command = FTP_CMD_STAT;
        }
        else if(command == "HELP")
        {
            ftpctx->command = FTP_CMD_HELP;
        }
        else if(command == "FEAT")
        {
            ftpctx->command = FTP_CMD_FEAT;
        }
        else if(command == "SIZE")
        {
            ftpctx->command = FTP_CMD_SIZE;
        }
        else if(command == "PROT")
        {
            ftpctx->command = FTP_CMD_PROT;
        }
        else if(command == "NOOP")
        {
            ftpctx->command = FTP_CMD_NOOP;
        }
        else if(command == "EPSV")
        {
            ftpctx->command = FTP_CMD_EPSV;
        }
        else if(command == "PBSZ")
        {
            ftpctx->command = FTP_CMD_PBSZ;
        }
        else if(command == "OPTS")
        {
            ftpctx->command = FTP_CMD_OPTS;
        }
        else
        {
            LOG_warn << " Could not match command: " << command;
            ftpctx->command = FTP_CMD_INVALID;
        }

        LOG_debug << " parsed command = " << ftpctx->command << " tcpctx=" << tcpctx;
        ftpctx->arg1 = "";
        ftpctx->arg2 = "";

        switch (ftpctx->command)
        {
        // no args
        case FTP_CMD_ABOR:
        case FTP_CMD_STOU:
        case FTP_CMD_QUIT:
        case FTP_CMD_REIN:
        case FTP_CMD_CDUP:
        case FTP_CMD_PASV:
        case FTP_CMD_EPSV:
        case FTP_CMD_PWD:
        case FTP_CMD_SYST:
        case FTP_CMD_FEAT:
        case FTP_CMD_NOOP:
            if (psep != string::npos)
            {
                parsed = -1;
            }
            break;
        // single arg
        case FTP_CMD_USER:
        case FTP_CMD_PASS:
        case FTP_CMD_ACCT:
        case FTP_CMD_CWD:
        case FTP_CMD_SMNT:
        case FTP_CMD_PORT:
        case FTP_CMD_TYPE:
        case FTP_CMD_STRU:
        case FTP_CMD_MODE:
        case FTP_CMD_RETR:
        case FTP_CMD_STOR:
        case FTP_CMD_APPE:
        case FTP_CMD_DELE:
        case FTP_CMD_RMD:
        case FTP_CMD_MKD:
        case FTP_CMD_REST:
        case FTP_CMD_RNFR:
        case FTP_CMD_RNTO:
        case FTP_CMD_SITE:
        case FTP_CMD_SIZE:
        case FTP_CMD_PBSZ:
        case FTP_CMD_PROT:
            if (psep != string::npos && ( (psep + 1)< petition.size()) )
            {
                string rest = petition.substr(psep+1);
                ftpctx->arg1 = rest;
            }
            else
            {
                parsed = -1;
            }

            break;
            //optional arg
        case FTP_CMD_LIST:
        case FTP_CMD_NLST:
        case FTP_CMD_STAT:
        case FTP_CMD_HELP:
            if (psep != string::npos)
            {
                string rest = petition.substr(psep+1);
                ftpctx->arg1 = rest;
            }
            break;

        case FTP_CMD_ALLO: //ALLO <SP> <decimal-integer> [<SP> R <SP> <decimal-integer>] <CRLF>
            if (psep != string::npos && ((psep + 1) < petition.size()))
            {
                string rest = petition.substr(psep+1);
                psep = rest.find_first_of(separators);
                ftpctx->arg1 = rest.substr(0,psep);
                if (psep != string::npos && ((psep + 1) < petition.size()))
                {  //optional R <SP> <decimal-integer>
                    rest = petition.substr(psep+1);
                    psep = rest.find_first_of(separators);
                    if (psep != 1 && (rest.at(0) != 'R' || rest.at(0) != 'r'))
                    {
                        parsed = -1;
                    }
                    else if ((psep + 1) < petition.size())
                    {
                        rest = petition.substr(psep+1);
                        ftpctx->arg2 = rest;
                    }
                    else
                    {
                        parsed = -1;
                    }
                }
            }
            else
            {
                parsed = -1;
            }
            break;
        case FTP_CMD_OPTS:
            if (psep != string::npos)
            {
                string rest = petition.substr(psep+1);
                psep = rest.find_first_of(separators);
                ftpctx->arg1 = rest.substr(0,psep);
                if (psep != string::npos && ((psep + 1) < rest.size()))
                {
                    ftpctx->arg2 = rest.substr(psep+1);
                }
                else
                {
                    parsed = -1;
                }
            }
            break;
        default:
            parsed = -1;
            break;
        };

        switch (ftpctx->command)
        {
        case FTP_CMD_USER:
        {
            response = "331 User name okay, need password";
            break;
        }
        case FTP_CMD_PASS:
        {
            response = "230 User logged in, proceed";

            MegaNode *n = ftpctx->megaApi->getRootNode();
            if (n)
            {
                ftpctx->cwd = n->getHandle();
                ftpctx->cwdpath = "/";
                ftpctx->atroot = true;
                ftpctx->athandle = false;
                delete n;
            }
            break;
        }
        case FTP_CMD_NOOP:
        {
            response = "200 NOOP from MEGA!";
            break;
        }
        case FTP_CMD_TYPE:
        case FTP_CMD_PROT: // we might want to require that arg1 = "P". or disable useTLS in data channel otherwise
        {
            response = "200 OK";
            break;
        }
        case FTP_CMD_PBSZ: //we don't use received buffer size (some client might fail)
        {
            response = "200 PBSZ=0";
            break;
        }
        case FTP_CMD_PASV:
        case FTP_CMD_EPSV:
        {
            if (!ftpctx->ftpDataServer)
            {
                if (pport > (dataPortEnd))
                {
                    pport = dataportBegin;
                }
                ftpctx->pasiveport = pport++;

                LOG_debug << "Creating new MegaFTPDataServer on port " << ftpctx->pasiveport;
#ifdef ENABLE_EVT_TLS
                MegaFTPDataServer *fds = new MegaFTPDataServer(megaApi, basePath, ftpctx, useTLS, certificatepath, keypath);
#else
                MegaFTPDataServer *fds = new MegaFTPDataServer(megaApi, basePath, ftpctx, useTLS, string(), string());
#endif
                bool result = fds->start(ftpctx->pasiveport, localOnly);
                if (result)
                {
                    ftpctx->ftpDataServer = fds;
                }
                else
                {
                    response = "421 Failed to initialize data channel";
                    break;
                }
            }
            else
            {
                LOG_debug << "Reusing FTP Data connection with port: " << ftpctx->pasiveport;
            }

            // gathering IP connected to
            struct sockaddr_in addr;
            int sadrlen = sizeof (struct sockaddr_in);
            uv_tcp_getsockname(&ftpctx->tcphandle,(struct sockaddr*)&addr, &sadrlen);
#ifdef WIN32
            string sIPtoPASV = inet_ntoa(addr.sin_addr);
#else
            char strIP[INET_ADDRSTRLEN];
            inet_ntop( AF_INET, &addr.sin_addr.s_addr, strIP, INET_ADDRSTRLEN);
            string sIPtoPASV = strIP;
#endif
            replace( sIPtoPASV.begin(), sIPtoPASV.end(), '.', ',');

            if (ftpctx->command == FTP_CMD_PASV)
            {
                char url[30];
                snprintf(url, sizeof(url), "%s,%d,%d", sIPtoPASV.c_str(), ftpctx->pasiveport/256, ftpctx->pasiveport%256);
                response = "227 Entering Passive Mode (";
                response.append(url);
                response.append(")");
            }
            else // FTP_CMD_EPSV
            {
                char url[30];
                snprintf(url, sizeof(url), "%d", ftpctx->pasiveport);
                response = "229 Entering Extended Passive Mode (|||";
                response.append(url);
                response.append("|)");
            }
            break;
        }
        case FTP_CMD_OPTS:
        {
            for (char& c : ftpctx->arg1) { c = static_cast<char>(toupper(c)); };
            for (char& c : ftpctx->arg2) { c = static_cast<char>(toupper(c)); };
            if (ftpctx->arg1 == "UTF8" && ftpctx->arg2 == "ON")
            {
                response = "200 All good";
            }
            else
            {
                response = "501 Unrecognized OPTS " + ftpctx->arg1 + " " + ftpctx->arg2;
            }
            break;
        }
        case FTP_CMD_PWD:
        {
            MegaNode *n = ftpctx->megaApi->getNodeByHandle(ftpctx->cwd);
            if (n)
            {
                response = "257 ";
                response.append("\"");
                response.append(ftpctx->cwdpath);
                response.append("\"");
                delete n;
            }
            else
            {
                response = "550 Not Found";
            }
            break;
        }
        case FTP_CMD_CWD:
        {
            response = cd(ftpctx->arg1, ftpctx);
            break;
        }
        case FTP_CMD_CDUP:
        {
            MegaNode *n = ftpctx->megaApi->getNodeByHandle(ftpctx->cwd);
            if (n)
            {
                handle parentHandle = n->getParentHandle();
                response = cdup(parentHandle, ftpctx);
                delete n;
            }
            else
            {
                response = "550 CWD not Found.";
            }
            break;
        }
        case FTP_CMD_LIST:
        case FTP_CMD_NLST:
        {
            if (!ftpctx->ftpDataServer)
            {
                response = "425 No Data Connection available";
                break;
            }

            MegaNode *node = NULL;
            if (ftpctx->arg1.size() && ftpctx->arg1 != "-l" && ftpctx->arg1 != "-a")
            {
                node = getNodeByFtpPath(ftpctx, ftpctx->arg1);
            }
            else
            {
                if (ftpctx->atroot)
                {
                    assert(!ftpctx->ftpDataServer->resultmsj.size());
                    set<handle> handles = getAllowedHandles();
                    for (std::set<handle>::iterator it = handles.begin(); it != handles.end(); ++it)
                    {
                        string name = megaApi->handleToBase64(*it);
                        MegaNode *n = megaApi->getNodeByHandle(*it);
                        if (n)
                        {
                            string toret = getListingLineFromNode(n, name);
                            toret.append(crlfout);
                            ftpctx->ftpDataServer->resultmsj.append(toret);
                            delete n;
                        }
                    }

                    ftpctx->ftpDataServer->resultmsj.append(crlfout);

                    response = "150 Here comes the directory listing";
                    break;
                }
                else if (ftpctx->athandle)
                {
                    MegaNode *n = megaApi->getNodeByHandle(ftpctx->cwd);
                    if (n)
                    {
                        string toret = getListingLineFromNode(n);
                        toret.append(crlfout);
                        assert(!ftpctx->ftpDataServer->resultmsj.size());
                        ftpctx->ftpDataServer->resultmsj.append(toret);
                        delete n;
                    }
                    ftpctx->ftpDataServer->resultmsj.append(crlfout);

                    response = "150 Here comes the directory listing";
                    break;
                }

                node = ftpctx->megaApi->getNodeByHandle(ftpctx->cwd);
            }
            if (node)
            {
                if (node->isFolder())
                {
                    MegaNodeList *children = ftpctx->megaApi->getChildren(node, MegaApi::ORDER_NONE);
                    assert(!ftpctx->ftpDataServer->resultmsj.size());

                    if (ftpctx->command == FTP_CMD_LIST)
                    {
                        ftpctx->ftpDataServer->resultmsj.append(getListingLineFromNode(node,"."));
                        ftpctx->ftpDataServer->resultmsj.append(crlfout);
                    }

                    for (int i = 0; i < children->size(); i++)
                    {
                        MegaNode *child = children->get(i);
                        //string childURL = subbaseURL + child->getName();
                        string toret;
                        if (ftpctx->command == FTP_CMD_LIST)
                        {
                            toret = getListingLineFromNode(child);
                        }
                        else //NLST
                        {
                            toret = child->getName();
                        }
                        toret.append(crlfout);
                        ftpctx->ftpDataServer->resultmsj.append(toret);
                    }
                    delete children;

                    ftpctx->ftpDataServer->resultmsj.append(crlfout);

                    response = "150 Here comes the directory listing";
                }
                else
                {
                    string toret;
                    if (ftpctx->command == FTP_CMD_LIST)
                    {
                        toret = getListingLineFromNode(node);
                    }
                    else //NLST
                    {
                        toret = node->getName();
                    }
                    toret.append(crlfout);
                    assert(!ftpctx->ftpDataServer->resultmsj.size());
                    ftpctx->ftpDataServer->resultmsj.append(toret);
                    response = "150 Here comes the file listing";
                }
                delete node;
            }
            else
            {
                response = "550 Not Found";
            }
            break;
        }
        case FTP_CMD_RETR:
        {
            if (!ftpctx->ftpDataServer)
            {
                response = "425 No Data Connection available";
                break;
            }

            MegaNode *node = getNodeByFtpPath(ftpctx, ftpctx->arg1);
            if (node)
            {
                if (node->isFile())
                {
                    uv_mutex_lock(&ftpctx->mutex_nodeToDownload);

                    MegaNode *oldNodeToDownload = ftpctx->ftpDataServer->nodeToDownload;
                    ftpctx->ftpDataServer->nodeToDownload = node;
                    delete oldNodeToDownload;

                    uv_mutex_unlock(&ftpctx->mutex_nodeToDownload);

                    response = "150 Here comes the file: ";
                    response.append(node->getName());
                }
                else
                {
                    response = "501 Not a file";
                    delete node;
                }
            }
            else
            {
                response = "550 Not Found";
            }
            break;
        }
//        case FTP_CMD_STOU: //do create random unique name
        case FTP_CMD_STOR:
        {
            //We might want to deal with foreign node / public link and so on ?
            if (!ftpctx->ftpDataServer)
            {
                response = "425 No Data Connection available";
                break;
            }

            MegaNode *newParentNode = NULL;
            size_t seppos = ftpctx->arg1.find_last_of("/");
            ftpctx->ftpDataServer->newNameToUpload = ftpctx->arg1;
            if (seppos == string::npos)
            {
                ftpctx->ftpDataServer->newParentNodeHandle = ftpctx->cwd;
            }
            else
            {
                if (!seppos) //new folder structure does not allow this
                {
                    response = "550 Not Found";
                    break;
                }

                if ((seppos + 1) < ftpctx->ftpDataServer->newNameToUpload.size())
                {
                    ftpctx->ftpDataServer->newNameToUpload = ftpctx->ftpDataServer->newNameToUpload.substr(seppos + 1);
                }
                string newparentpath = ftpctx->arg1.substr(0, seppos);
                newParentNode = getNodeByFtpPath(ftpctx, newparentpath);
                if (newParentNode)
                {
                    ftpctx->ftpDataServer->newParentNodeHandle = newParentNode->getHandle();
                    delete newParentNode;
                }
                else
                {
                    ftpctx->ftpDataServer->newNameToUpload = ""; //empty
                }
            }

            if (ftpctx->ftpDataServer->newNameToUpload.size())
            {
                ftpctx->ftpDataServer->remotePathToUpload = ftpctx->arg1;
                response = "150 Opening data connection for storing ";
                response.append(ftpctx->arg1);
            }
            else
            {
                response = "550 Destiny Not Found";
            }
            break;
        }
        case FTP_CMD_RNFR:
        {
            MegaNode *nodetoRename = getNodeByFtpPath(ftpctx, ftpctx->arg1);
            if (nodetoRename)
            {
                this->nodeHandleToRename = nodetoRename->getHandle();
                response = "350 Pending RNTO";
                delete nodetoRename;
            }
            else
            {
                response = "550 Not Found";
            }
            break;
        }
        case FTP_CMD_RMD:
        case FTP_CMD_DELE:
        {
            MegaNode *nodeToDelete = getNodeByFtpPath(ftpctx, ftpctx->arg1);

            if (nodeToDelete)
            {
                if ((ftpctx->command == FTP_CMD_DELE) ? nodeToDelete->isFile() : nodeToDelete->isFolder())
                {
                    ftpctx->megaApi->remove(nodeToDelete, false, ftpctx);
                    delayresponse = true;
                }
                else
                {
                    response = "501 Wrong type";
                }
                delete nodeToDelete;
            }
            else
            {
                response = "550 Not Found";
            }
            break;
        }
        case FTP_CMD_RNTO:
        {
            if (this->nodeHandleToRename == UNDEF)
            {
                response = "503 Bad sequence of commands, required RNFR first";
            }
            else
            {
                MegaNode *nodeToRename = ftpctx->megaApi->getNodeByHandle(this->nodeHandleToRename);
                if (nodeToRename)
                {
                    MegaNode *n = getNodeByFtpPath(ftpctx, ftpctx->arg1);
                    if (n)
                    {
                        ftpctx->nodeToDeleteAfterMove = n;
                    }

                    MegaNode *newParentNode = NULL;
                    size_t seppos = ftpctx->arg1.find_last_of("/");
                    string newName = ftpctx->arg1;

                    if (seppos != string::npos)
                    {
                        if (!seppos) //new folder structure does not allow this
                        {
                            response = "553 Requested action not taken: Invalid destiny";
                            delete n;
                            ftpctx->nodeToDeleteAfterMove = NULL;
                            delete nodeToRename;
                            break;
                        }

                        if ((seppos + 1) < newName.size())
                        {
                            newName = newName.substr(seppos + 1);
                        }
                        string newparentpath = ftpctx->arg1.substr(0, seppos);
                        newParentNode = getNodeByFtpPath(ftpctx, newparentpath);
                        if (!newParentNode)
                        {
                            newName = ""; //empty
                        }
                    }

                    if (newName.size())
                    {
                        if (newParentNode && newParentNode->getHandle() != nodeToRename->getParentHandle())
                        {
                            newNameAfterMove = newName;
                            ftpctx->megaApi->moveNode(nodeToRename, newParentNode, ftpctx);
                            delayresponse = true;
                        }
                        else
                        {
                            ftpctx->megaApi->renameNode(nodeToRename, newName.c_str(), ftpctx);
                            delayresponse = true;
                        }
                    }
                    else
                    {
                        response = "553 Requested action not taken. Invalid destiny";
                        delete n;
                        ftpctx->nodeToDeleteAfterMove = NULL;
                    }
                    delete newParentNode;
                    delete nodeToRename;
                }
                else
                {
                    response = "553 Requested action not taken. Origin not found: no longer available";
                }

                nodeHandleToRename = UNDEF;
            }
            break;
        }
        case FTP_CMD_MKD:
        {
            MegaNode *newParentNode = NULL;
            size_t seppos = ftpctx->arg1.find_last_of("/");
            string newNameFolder = ftpctx->arg1;
            MegaNode *n = getNodeByFtpPath(ftpctx, newNameFolder);
            if (n)
            {
                response = "550 already existing!";
                delete n;
            }
            else
            {
                if (!seppos) //new folder structure does not allow this
                {
                    response = "550 Not Found";
                    break;
                }

                if (seppos != string::npos)
                {
                    if ((seppos + 1) < newNameFolder.size())
                    {
                        newNameFolder = newNameFolder.substr(seppos + 1);
                    }
                    string newparentpath = ftpctx->arg1.substr(0, seppos);
                    newParentNode = getNodeByFtpPath(ftpctx, newparentpath);
                    if (!newParentNode)
                    {
                        newNameFolder = ""; //empty
                    }
                }

                if (newNameFolder.size())
                {
                    MegaNode *nodecwd = ftpctx->megaApi->getNodeByHandle(ftpctx->cwd);
                    ftpctx->megaApi->createFolder(newNameFolder.c_str(),newParentNode ? newParentNode : nodecwd, ftpctx);
                    delete nodecwd;
                    delayresponse = true;
                }
                else
                {
                    response = "550 Not Found";
                }
                delete newParentNode;
            }
            break;
        }
        case FTP_CMD_REST:
        {
            if (ftpctx->ftpDataServer && ftpctx->ftpDataServer->nodeToDownload)
            {
                unsigned long long number = strtoull(ftpctx->arg1.c_str(), NULL, 10);
                if (number != ULLONG_MAX)
                {
                    ftpctx->ftpDataServer->rangeStartREST = static_cast<m_off_t>(number);
                    response = "350 Restarting at: ";
                    response.append(ftpctx->arg1);
                }
                else
                {
                    response = "500 Syntax error, invalid start point";
                }
            }
            else
            {
                response = "350 Requested file action pending further information: Download file not available";
            }
            break;
        }
        case FTP_CMD_FEAT:
        {
            response = "211-Features:";
            response.append(crlfout);
            response.append(" SIZE");
            response.append(crlfout);
            response.append(" PROT");
            response.append(crlfout);
            response.append(" EPSV");
            response.append(crlfout);
            response.append(" PBSZ");
            response.append(crlfout);
//            response.append(" OPTS"); // This is actually compulsory when FEAT exists (no need to return it)
//            response.append(crlfout);
            response.append(" UTF8 ON");
            response.append(crlfout);
            response.append("211 End");
            break;
        }
        case FTP_CMD_SIZE: //This has to be exact, and depends ond STRU, MODE & TYPE!! (since assumed TYPE I, it's ok)
        {
            MegaNode *nodeToGetSize = getNodeByFtpPath(ftpctx, ftpctx->arg1);
            if (nodeToGetSize)
            {
                if (nodeToGetSize->isFile())
                {
                    response = "213 ";

                    ostringstream sizenumber;
                    sizenumber << nodeToGetSize->getSize();
                    response.append(sizenumber.str());
                }
                else
                {
                    response = "213 ";
                    response.append("0");
                }
                delete nodeToGetSize;
            }
            else
            {
                response = "550 Not Found";
            }
            break;
        }
        case FTP_CMD_INVALID:
        {
            response = "500 Syntax error, command unrecognized";
            break;
        }
        default:
            response = "502 Command not implemented";
            break;
        }

        response += crlfout;
    }

    if (nread < 0)
    {
        LOG_debug << "FTP Control Server received invalid read size. Closing connection";
        closeConnection(ftpctx);
    }
    else
    {
        if (!delayresponse)
        {
            LOG_verbose << " Processed: " << petition << ". command="<< ftpctx->command << ".\n   Responding: " << response << " tpctx=" << ftpctx;
            answer(ftpctx,response.c_str(),response.size());
        }
        else
        {
            LOG_verbose << " Processed: " << petition << ". command="<< ftpctx->command << ".\n   Delaying response. tpctx=" << ftpctx;
        }

        // Initiate data transfer for required commands
        if (ftpctx->ftpDataServer &&
                ( ftpctx->command == FTP_CMD_LIST
                  || ftpctx->command == FTP_CMD_NLST
                  || ftpctx->command == FTP_CMD_REST
                  || ftpctx->command == FTP_CMD_STOR
                  || ftpctx->command == FTP_CMD_RETR )
            )
        {
            LOG_debug << " calling sending data... ";
            ftpctx->ftpDataServer->sendData(); //rename to sth more sensefull: like wake data channel
        }
    }
    uv_mutex_unlock(&tcpctx->mutex);
}

void MegaFTPServer::processAsyncEvent(MegaTCPContext *tcpctx)
{
    LOG_verbose << "Processing FTP Server async event";
    if (tcpctx->finished)
    {
        LOG_debug << "FTP link closed, ignoring async event";
        return;
    }

    MegaFTPContext* ftpctx = dynamic_cast<MegaFTPContext *>(tcpctx);

    uv_mutex_lock(&ftpctx->mutex_responses);
    while (ftpctx->responses.size())
    {
        answer(tcpctx,ftpctx->responses.front().c_str(),ftpctx->responses.front().size());
        ftpctx->responses.pop_front();
    }
    uv_mutex_unlock(&ftpctx->mutex_responses);
}

void MegaFTPServer::processOnAsyncEventClose(MegaTCPContext*)
{
    LOG_verbose << "At MegaFTPServer::processOnAsyncEventClose";
}

void MegaTCPServer::answer(MegaTCPContext* tcpctx, const char *rsp, size_t rlen)
{
    LOG_verbose << " answering in port " << tcpctx->server->port << " : " << string(rsp,rlen);

    uv_buf_t resbuf = uv_buf_init((char *)rsp, static_cast<unsigned>(rlen));
#ifdef ENABLE_EVT_TLS
    if (tcpctx->server->useTLS)
    {
        // we are sending the response as a whole
        int err = evt_tls_write(tcpctx->evt_tls, resbuf.base, resbuf.len, onWriteFinished_tls);
        if (err <= 0)
        {
            LOG_warn << "Finishing due to an error sending the response: " << err;
            closeConnection(tcpctx);
        }
    }
    else
    {
#endif
        uv_write_t *req = new uv_write_t();
        req->data = tcpctx;
        if (int err = uv_write(req, (uv_stream_t*)&tcpctx->tcphandle, &resbuf, 1, onWriteFinished))
        {
            delete req;
            LOG_warn << "Finishing due to an error sending the response: " << err;
            closeTCPConnection(tcpctx);
        }
#ifdef ENABLE_EVT_TLS
    }
#endif
}

bool MegaFTPServer::respondNewConnection(MegaTCPContext* tcpctx)
{
    MegaFTPContext* ftpctx = dynamic_cast<MegaFTPContext *>(tcpctx);

    string response = "220 Wellcome to FTP MEGA Server";
    response.append(crlfout);

    answer(ftpctx, response.c_str(), response.size());
    return true;

}

void MegaFTPServer::processOnExitHandleClose(MegaTCPServer*) {}

MegaFTPContext::MegaFTPContext()
{
    command = 0;
    resultcode = 0;

    cwd = UNDEF;
    atroot = false;
    athandle = false;
    parentcwd = UNDEF;
    pasiveport = -1;
    ftpDataServer = NULL;
    nodeToDeleteAfterMove = NULL;
    uv_mutex_init(&mutex_responses);
    uv_mutex_init(&mutex_nodeToDownload);
}

MegaFTPContext::~MegaFTPContext()
{
    if (ftpDataServer)
    {
        LOG_verbose << "Deleting ftpDataServer associated with ftp context";
        delete ftpDataServer;
    }
    if (tmpFileName.size())
    {
        LocalPath localPath = LocalPath::fromAbsolutePath(tmpFileName);
        server->fsAccess->unlinklocal(localPath);
        tmpFileName = "";
    }
    uv_mutex_destroy(&mutex_responses);
    uv_mutex_destroy(&mutex_nodeToDownload);
}

void MegaFTPContext::onTransferStart(MegaApi*, MegaTransfer*) {}

bool MegaFTPContext::onTransferData(MegaApi*, MegaTransfer*, char* /*buffer*/, size_t /*size*/)
{
    LOG_verbose << "MegaFTPContext::onTransferData";
    return true;
}

void MegaFTPContext::onTransferFinish(MegaApi *, MegaTransfer *, MegaError *e)
{
    if (finished)
    {
        LOG_debug << "FTP link closed, ignoring the result of the transfer";
        return;
    }

    if (e->getErrorCode() == MegaError::API_OK)
    {
        MegaFTPServer::returnFtpCodeAsync(this, 250);
    }
    else
    {
        MegaFTPServer::returnFtpCodeAsyncBasedOnRequestError(this, e);
    }
    if (tmpFileName.size())
    {
        LocalPath localPath = LocalPath::fromAbsolutePath(tmpFileName);
        server->fsAccess->unlinklocal(localPath);
        tmpFileName = "";
    }
}

void MegaFTPContext::onRequestFinish(MegaApi *, MegaRequest *request, MegaError *e)
{
    if (finished)
    {
        LOG_debug << "HTTP link closed, ignoring the result of the request";
        return;
    }

    MegaFTPServer* ftpserver = dynamic_cast<MegaFTPServer *>(this->server);

    if (request->getType() == MegaRequest::TYPE_REMOVE)
    {
        if (e->getErrorCode() == MegaError::API_OK)
        {
            if (cwd == request->getNodeHandle())
            {
                LOG_verbose << " Removing cwd node, going back to parent";
                ftpserver->cdup(parentcwd, this);
            }
            else // This could be unexpected if CWD is removed elsewhere by the time this RequestFinish completes //TODO: decide upon revision: is this OK?
            {
                MegaNode *n = megaApi->getNodeByHandle(cwd);
                size_t seps = static_cast<size_t>(std::count(cwdpath.begin(), cwdpath.end(), '/'));
                unsigned int isep = 0;
                string sup = cwdpath;
                while (!n && (isep++ < seps))
                {
                    sup.append("/..");
                    string nsup = ftpserver->shortenpath(sup);
                    ftpserver->cd(nsup, this);
                    n = megaApi->getNodeByHandle(cwd);
                }
                delete n;
            }
            MegaFTPServer::returnFtpCodeAsync(this, 250);
        }
        else
        {
            MegaFTPServer::returnFtpCodeAsyncBasedOnRequestError(this, e);
        }
    }
    else if (request->getType() == MegaRequest::TYPE_CREATE_FOLDER)
    {
        if (e->getErrorCode() == MegaError::API_OK)
        {
            MegaFTPServer::returnFtpCodeAsync(this, 257, request->getName());
        }
        else
        {
            MegaFTPServer::returnFtpCodeAsyncBasedOnRequestError(this, e);
        }
    }
    else if (request->getType() == MegaRequest::TYPE_RENAME)
    {
        if (e->getErrorCode() == MegaError::API_OK )
        {
            if (nodeToDeleteAfterMove)
            {
                this->megaApi->remove(nodeToDeleteAfterMove, false, this);
                nodeToDeleteAfterMove = NULL;
                delete nodeToDeleteAfterMove;
            }
            else
            {
                MegaFTPServer::returnFtpCodeAsync(this, 250);
            }
        }
        else
        {
            MegaFTPServer::returnFtpCodeAsyncBasedOnRequestError(this, e);
        }
    }
    else  if (request->getType() == MegaRequest::TYPE_MOVE)
    {
        if (e->getErrorCode() == MegaError::API_OK)
        {
            if (ftpserver->newNameAfterMove.size())
            {
                MegaNode *nodetoRename = this->megaApi->getNodeByHandle(request->getNodeHandle());
                if (!nodetoRename)
                {
                    MegaFTPServer::returnFtpCodeAsync(this, 550, "Moved node not found");
                }
                else if (!strcmp(nodetoRename->getName(), ftpserver->newNameAfterMove.c_str()))
                {
                    if (nodeToDeleteAfterMove)
                    {
                        this->megaApi->remove(nodeToDeleteAfterMove, false, this);
                        nodeToDeleteAfterMove = NULL;
                        delete nodeToDeleteAfterMove;
                    }
                    else
                    {
                        MegaFTPServer::returnFtpCodeAsync(this, 250);
                    }
                }
                else
                {
                    this->megaApi->renameNode(nodetoRename, ftpserver->newNameAfterMove.c_str(), this);
                }
                delete nodetoRename;
            }
            else
            {
                if (nodeToDeleteAfterMove)
                {
                    this->megaApi->remove(nodeToDeleteAfterMove, false, this);
                    nodeToDeleteAfterMove = NULL;
                    delete nodeToDeleteAfterMove;
                }
                else
                {
                    MegaFTPServer::returnFtpCodeAsync(this, 250);
                }
            }
        }
        else
        {
            MegaFTPServer::returnFtpCodeAsyncBasedOnRequestError(this, e);
        }
    }

    uv_async_send(&asynchandle);
}


// FTP DATA Server
MegaFTPDataServer::MegaFTPDataServer(MegaApiImpl *megaApi, string basePath, MegaFTPContext * controlftpctx, bool useTLS, string certificatepath, string keypath)
: MegaTCPServer(megaApi, basePath, useTLS, certificatepath, keypath)
{
    this->controlftpctx = controlftpctx;
    this->nodeToDownload = NULL;
    this->rangeStartREST = 0;
    this->notifyNewConnectionRequired = false;
    this->newParentNodeHandle = UNDEF;
}

MegaFTPDataServer::~MegaFTPDataServer()
{
    LOG_verbose << "MegaFTPDataServer::~MegaFTPDataServer";
    delete nodeToDownload;

    // if not stopped, the uv thread might want to access a pointer to this.
    // though this is done in the parent destructor, it could try to access it after vtable has been erased
    stop();
    LOG_verbose << "MegaFTPDataServer::~MegaFTPDataServer. end";
}

MegaTCPContextPtr MegaFTPDataServer::initializeContext(uv_stream_t* server_handle)
{
    shared_ptr<MegaFTPDataContext> ftpctx = std::make_shared<MegaFTPDataContext>();

    // Set connection data
    MegaFTPDataServer* ftpDataServer = (MegaFTPDataServer*)(server_handle->data);
    ftpctx->server = ftpDataServer;
    ftpctx->megaApi = ftpDataServer->megaApi;
    ftpctx->tcphandle.data = ftpctx.get();
    ftpctx->asynchandle.data = ftpctx.get();

    return ftpctx;
}

void MegaFTPDataServer::processWriteFinished(MegaTCPContext *tcpctx, int status)
{
    if (status < 0)
    {
        LOG_warn << " error received at processWriteFinished: " << status << ": " << uv_err_name(status);
    }

    MegaFTPDataContext* ftpdatactx = dynamic_cast<MegaFTPDataContext *>(tcpctx);

    LOG_debug << " processWriteFinished on MegaFTPDataServer. status = " << status;
    if (resultmsj.size())
    {
        resultmsj = ""; // empty result msj // this would be incorrect if we used partial writes (does not seem to be required)

        if (this->controlftpctx)
        {
            ftpdatactx->setControlCodeUponDataClose(226);
        }
        else
        {
            LOG_verbose << "Avoiding waking controlftp aync handle, ftpctx already closed";
        }
        closeConnection(tcpctx);
    }
    else // transfering node (download)
    {
        ftpdatactx->bytesWritten += ftpdatactx->lastBufferLen;
        LOG_verbose << "Bytes written: " << ftpdatactx->lastBufferLen << " Remaining: " << (ftpdatactx->size - ftpdatactx->bytesWritten);
        ftpdatactx->lastBuffer = NULL;

        if (status < 0 || ftpdatactx->size == ftpdatactx->bytesWritten)
        {
            if (status < 0)
            {
                LOG_warn << "Finishing request. Write failed: " << status << ": " << uv_err_name(status);
            }
            else
            {
                LOG_debug << "Finishing request. All data sent";
            }

            if (this->controlftpctx)
            {
                ftpdatactx->setControlCodeUponDataClose(226);
            }
            else
            {
                LOG_verbose << "Avoiding waking controlftp aync handle, ftpctx already closed";
            }
            closeConnection(ftpdatactx);
            return;
        }

        uv_mutex_lock(&ftpdatactx->mutex);
        if (ftpdatactx->lastBufferLen)
        {
            ftpdatactx->streamingBuffer.freeData(ftpdatactx->lastBufferLen);
            ftpdatactx->lastBufferLen = 0;
        }

        if (ftpdatactx->pause)
        {
            if (ftpdatactx->streamingBuffer.availableSpace() > ftpdatactx->streamingBuffer.availableCapacity() / 2)
            {
                ftpdatactx->pause = false;
                m_off_t start = ftpdatactx->rangeStart + ftpdatactx->rangeWritten +
                                static_cast<m_off_t>(ftpdatactx->streamingBuffer.availableData());
                m_off_t len = ftpdatactx->rangeEnd - ftpdatactx->rangeStart -
                              ftpdatactx->rangeWritten -
                              static_cast<m_off_t>(ftpdatactx->streamingBuffer.availableData());

                LOG_debug << "[Streaming] Resuming streaming from " << start << " len: " << len
                          << " " << ftpdatactx->streamingBuffer.bufferStatus();
                ftpdatactx->megaApi->startStreaming(ftpdatactx->node, start, len, ftpdatactx);
            }
        }
        uv_mutex_unlock(&ftpdatactx->mutex);

        uv_async_send(&ftpdatactx->asynchandle);
    }
}

void MegaFTPDataServer::sendData()
{
    this->notifyNewConnectionRequired = true;

    // only interested in the last connection received (the one that needs response)
    // Some client might create connections before receiving a 150 in the control channel (e.g: ftp
    // linux command)
    // This could cause never answered / never closed connections.
    // assert(connections.size() <= 1); //This might not be true due to that
    MegaTCPContext* tcpctx = connections.back();

    if (tcpctx)
    {
        LOG_verbose << "MegaFTPDataServer::sendData. triggering asyncsend for tcpctx=" << tcpctx;
#ifdef ENABLE_EVT_TLS
        if (tcpctx->evt_tls == NULL)
        {
            LOG_warn << "MegaFTPDataServer::sendData, evt_tls is NULL";
        }

        if (useTLS && (!tcpctx->evt_tls || tcpctx->finished || !evt_tls_is_handshake_over(tcpctx->evt_tls)))
        {
            if (!tcpctx->evt_tls)
            {
                LOG_verbose << "MegaFTPDataServer::sendData. no evt_tls";
            }
            else if (tcpctx->finished)
            {
                LOG_verbose << "MegaFTPDataServer::sendData. tcpctx->finished";
                this->notifyNewConnectionRequired = false;
            }
            else
            {
                LOG_verbose << "MegaFTPDataServer::sendData. handshake not over";
            }
        }
        else
        {
#endif
            LOG_verbose << "MegaFTPDataServer::sendData. do triggering asyncsend 03";
            this->notifyNewConnectionRequired = false;
            uv_async_send(&tcpctx->asynchandle);
#ifdef ENABLE_EVT_TLS
        }
#endif
    }
    else
    {
        LOG_verbose << "MegaFTPDataServer::sendData. no tcpctx. notifyNewConnectionRequired";
        this->notifyNewConnectionRequired = true;
    }
}

void MegaFTPDataServer::processReceivedData(MegaTCPContext *tcpctx, ssize_t nread, const uv_buf_t * buf)
{
    MegaFTPDataContext* ftpdatactx = dynamic_cast<MegaFTPDataContext *>(tcpctx);
    MegaFTPDataServer* fds = dynamic_cast<MegaFTPDataServer *>(ftpdatactx->server);

    if (fds->newNameToUpload.size())
    {
        //create tmp file with contents in messageBody
        if (!ftpdatactx->tmpFileAccess)
        {
            ftpdatactx->tmpFileName = fds->basePath;
            ftpdatactx->tmpFileName.append("ftpstorfile");
            ftpdatactx->tmpFileName.append(LocalPath::tmpNameLocal().toPath(false));

            string ext;
            if (ftpdatactx->server->fsAccess->getextension(LocalPath::fromAbsolutePath(fds->controlftpctx->arg1), ext))
            {
                ftpdatactx->tmpFileName.append(ext);
            }

            ftpdatactx->tmpFileAccess = fds->fsAccess->newfileaccess();
            LocalPath localPath = LocalPath::fromAbsolutePath(ftpdatactx->tmpFileName);
            fds->fsAccess->unlinklocal(localPath);

            if (!ftpdatactx->tmpFileAccess->fopen(localPath, OPEN_WRONLY, FSLogging::logOnError))
            {
                ftpdatactx->setControlCodeUponDataClose(450);
                remotePathToUpload = ""; //empty, so that we don't read in the next connections
                closeConnection(tcpctx);
                return;
            }
        }

        if (nread > 0)
        {
            LOG_verbose << " Writing " << nread << " bytes " << " to temporal file: " << ftpdatactx->tmpFileName;
            if (!ftpdatactx->tmpFileAccess->fwrite(reinterpret_cast<const byte*>(buf->base),
                                                   static_cast<unsigned>(nread),
                                                   static_cast<m_off_t>(ftpdatactx->tmpFileSize)))
            {
                ftpdatactx->setControlCodeUponDataClose(450);
                remotePathToUpload = ""; //empty, so that we don't read in the next connections
                closeConnection(tcpctx);
            }
            ftpdatactx->tmpFileSize += static_cast<size_t>(nread);
        }
    }
    else
    {
        LOG_err << "FTPData server receiving unexpected data: " << nread << " bytes";
    }


    if (nread < 0) //transfer finish
    {
        LOG_verbose << "FTP Data Channel received invalid read size: " << nread << ". Closing connection";
        if (ftpdatactx->tmpFileName.size())
        {
            std::unique_ptr<MegaNode> newParentNode {ftpdatactx->megaApi->getNodeByHandle(fds->newParentNodeHandle)};
            if (newParentNode)
            {
                LOG_debug << "Starting upload of file " << fds->newNameToUpload;
                fds->controlftpctx->tmpFileName = ftpdatactx->tmpFileName;

                FileSystemType fsType = fds->fsAccess->getlocalfstype(LocalPath::fromAbsolutePath(ftpdatactx->tmpFileName));
                MegaApiImpl::MegaUploadOptionsPrivate uploadOptions;
                uploadOptions.mPublicOptions.fileName = fds->newNameToUpload;
                uploadOptions.mPublicOptions.mtime = -1;
                uploadOptions.mIsBackup = true;
                uploadOptions.mFsType = fsType;

                ftpdatactx->megaApi->startUpload(ftpdatactx->tmpFileName,
                                                 newParentNode->getHandle(),
                                                 CancelToken(),
                                                 uploadOptions,
                                                 fds->controlftpctx);

                ftpdatactx->controlRespondedElsewhere = true;
            }
            else
            {
                LOG_err << "Unable to start upload: " << fds->newNameToUpload;
                ftpdatactx->setControlCodeUponDataClose(550, "Destination folder not available");
            }
            ftpdatactx->tmpFileName="";
        }
        else
        {
            LOG_err << "Data channel received close without tmp file created!";
            ftpdatactx->setControlCodeUponDataClose(426);
        }

        ftpdatactx->tmpFileName = ""; // empty so that we don't try to upload it
        remotePathToUpload = ""; //empty, so that we don't read in the next connections
        closeConnection(tcpctx);
        return;
    }
}

void MegaFTPDataServer::processAsyncEvent(MegaTCPContext *tcpctx)
{
    LOG_verbose << "MegaFTPDataServer::processAsyncEvent. tcptcx= " << tcpctx;
    MegaFTPDataContext* ftpdatactx = dynamic_cast<MegaFTPDataContext *>(tcpctx);
    MegaFTPDataServer* fds = dynamic_cast<MegaFTPDataServer *>(tcpctx->server);

    if (ftpdatactx->finished)
    {
        LOG_debug << "FTP DATA link closed, ignoring async event";
        return;
    }

    if (ftpdatactx->failed)
    {
        LOG_warn << "Streaming transfer failed. Closing connection.";
        closeConnection(ftpdatactx);
        return;
    }


    uv_mutex_lock(&fds->controlftpctx->mutex_nodeToDownload);

    if (resultmsj.size())
    {
        LOG_debug << " responding DATA: " << resultmsj;
        answer(ftpdatactx, resultmsj.c_str(), resultmsj.size());
    }
    else if (remotePathToUpload.size())
    {
        LOG_debug << " receive data to store in tmp file:";
        readData(ftpdatactx);
    }
    else if (nodeToDownload)
    {
        if (!ftpdatactx->node || rangeStartREST) //alterantive to || rangeStart, define aborted?
        {
            if (!rangeStartREST)
            {
                LOG_debug << "Initiating node download via port " << fds->port;
            }
            else
            {
                LOG_debug << "Initiating node download from: " << rangeStartREST << " via port " << fds->port;
            }

            ftpdatactx->rangeStart = rangeStartREST;
            rangeStartREST = 0;// so as not to start again
            ftpdatactx->bytesWritten = 0;
            ftpdatactx->size = 0;
            ftpdatactx->streamingBuffer.setMaxBufferSize(
                static_cast<unsigned>(ftpdatactx->server->getMaxBufferSize()));
            ftpdatactx->streamingBuffer.setMaxOutputSize(
                static_cast<unsigned>(ftpdatactx->server->getMaxOutputSize()));

            ftpdatactx->transfer = new MegaTransferPrivate(MegaTransfer::TYPE_LOCAL_TCP_DOWNLOAD);

            ftpdatactx->transfer->setPath(fds->controlftpctx->arg1.c_str());
            if (ftpdatactx->nodename.size())
            {
                ftpdatactx->transfer->setFileName(ftpdatactx->nodename.c_str());
            }
            if (ftpdatactx->nodehandle.size())
            {
                ftpdatactx->transfer->setNodeHandle(MegaApi::base64ToHandle(ftpdatactx->nodehandle.c_str()));
            }

            ftpdatactx->transfer->setStartTime(Waiter::ds);

            m_off_t totalSize = nodeToDownload->getSize();
            m_off_t start = 0;
            m_off_t end = totalSize - 1;
            if (ftpdatactx->rangeStart > 0)
            {
                start = ftpdatactx->rangeStart;
            }
            ftpdatactx->rangeEnd = end + 1;
            ftpdatactx->rangeStart = start;

            //bool rangeRequested = (/*ftpdatactx->rangeEnd*/ - ftpdatactx->rangeStart) != totalSize;
            m_off_t len = end - start + 1;

            ftpdatactx->pause = false;
            ftpdatactx->lastBuffer = NULL;
            ftpdatactx->lastBufferLen = 0;
            ftpdatactx->transfer->setStartPos(0);
            ftpdatactx->transfer->setEndPos(end); //This will actually be override later
            ftpdatactx->node = nodeToDownload->copy();

            ftpdatactx->streamingBuffer.setFileSize(nodeToDownload->getSize());
            ftpdatactx->streamingBuffer.setDuration(nodeToDownload->getDuration());

            ftpdatactx->streamingBuffer.init(static_cast<size_t>(len));
            ftpdatactx->size = len;

            ftpdatactx->megaApi->fireOnFtpStreamingStart(ftpdatactx->transfer);

            LOG_debug << "Requesting range. From " << start << "  size " << len;
            ftpdatactx->rangeWritten = 0;
            if (start || len)
            {
                ftpdatactx->megaApi->startStreaming(nodeToDownload, start, len, ftpdatactx);
            }
            else
            {
                LOG_debug << "Skipping startStreaming call since empty file";
                ftpdatactx->megaApi->fireOnFtpStreamingFinish(ftpdatactx->transfer, std::make_unique<MegaErrorPrivate>(API_OK));
                ftpdatactx->transfer = NULL; // this has been deleted in fireOnStreamingFinish
                fds->processWriteFinished(ftpdatactx, 0);
            }
        }
        else
        {
            LOG_debug << "Calling sendNextBytes port = " << fds->port;
            sendNextBytes(ftpdatactx);
        }
    }
    else
    {

        LOG_err << " Async event with no result mesj!!!";
    }

    uv_mutex_unlock(&fds->controlftpctx->mutex_nodeToDownload);
}

void MegaFTPDataServer::processOnAsyncEventClose(MegaTCPContext* tcpctx)
{
    MegaFTPDataContext* ftpdatactx = dynamic_cast<MegaFTPDataContext *>(tcpctx);
    MegaFTPDataServer *fds = ((MegaFTPDataServer *)ftpdatactx->server);

    LOG_verbose << "MegaFTPDataServer::processOnAsyncEventClose. tcpctx=" << tcpctx << " port = " << fds->port << " remaining = " << fds->remainingcloseevents;

    fds->remotePathToUpload = "";

    if (ftpdatactx->transfer)
    {
        ftpdatactx->megaApi->cancelTransfer(ftpdatactx->transfer);
        ftpdatactx->megaApi->fireOnFtpStreamingFinish(ftpdatactx->transfer, std::make_unique<MegaErrorPrivate>(ftpdatactx->ecode));
        ftpdatactx->transfer = NULL; // this has been deleted in fireOnStreamingFinish
    }

    if (!fds->remainingcloseevents && fds->closing)
    {
        LOG_verbose << "MegaFTPDataServer::processOnAsyncEventClose stopping without waiting. port = " << fds->port;
        fds->stop(true);
    }

    if (!ftpdatactx->controlRespondedElsewhere && fds->started && !this->controlftpctx->finished)
    {
        LOG_debug << "MegaFTPDataServer::processOnAsyncEventClose port = " << fds->port << ". Responding " << ftpdatactx->controlResponseCode << ". " << ftpdatactx->controlResponseMessage;
        MegaFTPServer* ftpControlServer = dynamic_cast<MegaFTPServer *>(fds->controlftpctx->server);
        ftpControlServer->returnFtpCode(this->controlftpctx, ftpdatactx->controlResponseCode, ftpdatactx->controlResponseMessage);
    }
}

bool MegaFTPDataServer::respondNewConnection(MegaTCPContext* tcpctx)
{
    MegaFTPDataContext* ftpdatactx = dynamic_cast<MegaFTPDataContext *>(tcpctx);
    if (notifyNewConnectionRequired) //in some cases, control connection tried this before the ftpdatactx was ready. this fixes that
    {
        LOG_verbose << "MegaFTPDataServer::respondNewConnection async sending to notify new connection";
        uv_async_send(&ftpdatactx->asynchandle);
        notifyNewConnectionRequired = false;
    }

    return false;
}

void MegaFTPDataServer::processOnExitHandleClose(MegaTCPServer*) {}

void MegaFTPDataServer::sendNextBytes(MegaFTPDataContext *ftpdatactx)
{
    if (ftpdatactx->finished)
    {
        LOG_debug << "FTP link closed, aborting write";
        return;
    }

    if (ftpdatactx->lastBuffer)
    {
        LOG_verbose << "[Streaming] Skipping write due to another ongoing write";
        return;
    }

    uv_mutex_lock(&ftpdatactx->mutex);
    if (ftpdatactx->lastBufferLen)
    {
        ftpdatactx->streamingBuffer.freeData(ftpdatactx->lastBufferLen);
        ftpdatactx->lastBufferLen = 0;
    }

    if (ftpdatactx->tcphandle.write_queue_size > ftpdatactx->streamingBuffer.availableCapacity() / 8)
    {
        LOG_warn << "[Streaming] Skipping write. Too much queued data. " << ftpdatactx->streamingBuffer.bufferStatus();
        uv_mutex_unlock(&ftpdatactx->mutex);
        return;
    }

    uv_buf_t resbuf = ftpdatactx->streamingBuffer.nextBuffer();
    uv_mutex_unlock(&ftpdatactx->mutex);

    if (!resbuf.len)
    {
        LOG_verbose << "[Streaming] Skipping write. No data available. " << ftpdatactx->streamingBuffer.bufferStatus();
        return;
    }

    LOG_verbose << "Writing " << resbuf.len << " bytes" << " buffered = " << ftpdatactx->streamingBuffer.availableData();
    ftpdatactx->rangeWritten += resbuf.len;
    ftpdatactx->lastBuffer = resbuf.base;
    ftpdatactx->lastBufferLen = resbuf.len;

#ifdef ENABLE_EVT_TLS
    if (ftpdatactx->server->useTLS)
    {
        //notice this, contrary to !useTLS is synchronous
        int err = evt_tls_write(ftpdatactx->evt_tls, resbuf.base, resbuf.len, onWriteFinished_tls);
        if (err <= 0)
        {
            LOG_warn << "[Streaming] Finishing due to an error sending the response: " << err;
            closeConnection(ftpdatactx);
        }
    }
    else
    {
#endif
        uv_write_t *req = new uv_write_t();
        req->data = ftpdatactx;

        if (int err = uv_write(req, (uv_stream_t*)&ftpdatactx->tcphandle, &resbuf, 1, onWriteFinished))
        {
            delete req;
            LOG_warn << "[Streaming] Finishing due to an error in uv_write: " << err;
            closeTCPConnection(ftpdatactx);
        }
#ifdef ENABLE_EVT_TLS
    }
#endif
}


MegaFTPDataContext::MegaFTPDataContext()
{
    transfer = NULL;
    lastBuffer = NULL;
    lastBufferLen = 0;
    failed = false;
    ecode = API_OK;
    pause = false;
    node = NULL;
    rangeWritten = 0;
    rangeStart = 0;
    rangeEnd = 0;
    tmpFileAccess = NULL;
    tmpFileSize = 0;
    controlRespondedElsewhere = false;
    controlResponseCode = 426;
}

MegaFTPDataContext::~MegaFTPDataContext()
{
    delete transfer;
    delete node;
}

void MegaFTPDataContext::setControlCodeUponDataClose(int code, string msg)
{
    controlResponseCode = code;
    controlResponseMessage = msg;
}

void MegaFTPDataContext::onTransferStart(MegaApi*, MegaTransfer* ftpDataTransfer)
{
    transfer->setTag(ftpDataTransfer->getTag());
}

bool MegaFTPDataContext::onTransferData(MegaApi*,
                                        MegaTransfer* ftpDataTransfer,
                                        char* buffer,
                                        size_t dataSize)
{
    LOG_verbose << "Streaming data received: " << ftpDataTransfer->getTransferredBytes()
                << " Size: " << dataSize << " Remaining from transfer: "
                << (dataSize + static_cast<size_t>(ftpDataTransfer->getTotalBytes() -
                                                   ftpDataTransfer->getTransferredBytes()))
                << " Remaining to write TCP: " << (size - bytesWritten)
                << " Queued: " << this->tcphandle.write_queue_size
                << " Buffered: " << streamingBuffer.availableData()
                << " Free: " << streamingBuffer.availableSpace();

    if (finished)
    {
        LOG_info << "Removing streaming transfer after " << ftpDataTransfer->getTransferredBytes()
                 << " bytes";
        return false;
    }

    // append the data to the buffer
    uv_mutex_lock(&mutex);
    long long remaining =
        static_cast<long long>(dataSize) +
        (ftpDataTransfer->getTotalBytes() - ftpDataTransfer->getTransferredBytes());
    long long availableSpace = static_cast<long long>(streamingBuffer.availableSpace());
    if (remaining > availableSpace && availableSpace < (2 * m_off_t(dataSize)))
    {
        LOG_debug << "[Streaming] Buffer full: Pausing streaming. " << streamingBuffer.bufferStatus();
        pause = true;
    }
    streamingBuffer.append(buffer, dataSize);
    uv_mutex_unlock(&mutex);

    // notify the HTTP server
    uv_async_send(&asynchandle);
    return !pause;
}

void MegaFTPDataContext::onTransferFinish(MegaApi *, MegaTransfer *, MegaError *e)
{
    LOG_verbose << "MegaFTPDataContext::onTransferFinish";
    if (finished)
    {
        LOG_debug << "FTP Data link closed";
        return;
    }
    ecode = e->getErrorCode();
    if (ecode != API_OK && ecode != API_EINCOMPLETE)
    {
        LOG_warn << "Transfer failed with error code: " << ecode;
        failed = true;
    }
    uv_async_send(&asynchandle);
}

void MegaFTPDataContext::onRequestFinish(MegaApi*, MegaRequest*, MegaError*)
{
    if (finished)
    {
        LOG_debug << "FTP data link closed, ignoring the result of the request";
        return;
    }

    uv_async_send(&asynchandle);
}

#endif

#ifdef ENABLE_CHAT
MegaTextChatPeerListPrivate::MegaTextChatPeerListPrivate()
{

}

MegaTextChatPeerListPrivate::~MegaTextChatPeerListPrivate()
{

}

MegaTextChatPeerList *MegaTextChatPeerListPrivate::copy() const
{
    MegaTextChatPeerListPrivate *ret = new MegaTextChatPeerListPrivate;

    for (size_t i = 0; i < static_cast<size_t>(size()); i++)
    {
        ret->addPeer(list.at(i).first, list.at(i).second);
    }

    return ret;
}

void MegaTextChatPeerListPrivate::addPeer(MegaHandle h, int priv)
{
    list.push_back(userpriv_pair(h, (privilege_t) priv));
}

MegaHandle MegaTextChatPeerListPrivate::getPeerHandle(int i) const
{
    if (i > size())
    {
        return INVALID_HANDLE;
    }
    else
    {
        return list.at(static_cast<size_t>(i)).first;
    }
}

int MegaTextChatPeerListPrivate::getPeerPrivilege(int i) const
{
    if (i > size())
    {
        return PRIV_UNKNOWN;
    }
    else
    {
        return list.at(static_cast<size_t>(i)).second;
    }
}

int MegaTextChatPeerListPrivate::size() const
{
    return int(list.size());
}

const userpriv_vector *MegaTextChatPeerListPrivate::getList() const
{
    return &list;
}

void MegaTextChatPeerListPrivate::setPeerPrivilege(handle uh, privilege_t priv)
{
    for (unsigned int i = 0; i < list.size(); i++)
    {
        if (list.at(i).first == uh)
        {
            list.at(i).second = priv;
            return;
        }
    }

    // handle not found. Create new item
    addPeer(uh, priv);
}

MegaTextChatPeerListPrivate::MegaTextChatPeerListPrivate(const userpriv_vector *userpriv)
{
    handle uh;
    privilege_t priv;

    for (unsigned i = 0; i < userpriv->size(); i++)
    {
        uh = userpriv->at(i).first;
        priv = userpriv->at(i).second;

        this->addPeer(uh, priv);
    }
}

MegaTextChatPrivate::MegaTextChatPrivate(const MegaTextChat *chat)
{
    this->id = chat->getHandle();
    this->priv = chat->getOwnPrivilege();
    this->shard = chat->getShard();
    this->peers = chat->getPeerList() ? chat->getPeerList()->copy() : NULL;
    this->group = chat->isGroup();
    this->ou = chat->getOriginatingUser();
    this->title = chat->getTitle() ? chat->getTitle() : "";
    this->ts = chat->getCreationTime();
    this->archived = chat->isArchived();
    this->publicchat = chat->isPublicChat();
    this->tag = chat->isOwnChange();
    this->changed = chat->getChanges();
    this->unifiedKey = chat->getUnifiedKey() ? chat->getUnifiedKey() : "";
    this->meeting = chat->isMeeting();
    this->chatOptions = chat->getChatOptions();

    if (chat->getScheduledMeetingList() && chat->getScheduledMeetingList()->size())
    {
        mScheduledMeetings.reset(chat->getScheduledMeetingList()->copy());
    }

    if (chat->getSchedMeetingsChanged() && chat->getSchedMeetingsChanged()->size())
    {
        mSchedMeetingsChanged.reset(chat->getSchedMeetingsChanged()->copy());
    }

    if (chat->getUpdatedOccurrencesList() && chat->getUpdatedOccurrencesList()->size())
    {
        mUpdatedOcurrences.reset(chat->getUpdatedOccurrencesList()->copy());
    }
}

MegaTextChatPrivate::MegaTextChatPrivate(const TextChat *chat)
{
    this->id = chat->getChatId();
    this->priv = chat->getOwnPrivileges();
    this->shard = chat->getShard();
    this->peers = chat->getUserPrivileges() ? new MegaTextChatPeerListPrivate(chat->getUserPrivileges()) : nullptr;
    this->group = chat->getGroup();
    this->ou = chat->getOwnUser();
    this->title = chat->getTitle();
    this->tag = chat->getTag();
    this->ts = chat->getTs();
    this->archived = chat->isFlagSet(TextChat::FLAG_OFFSET_ARCHIVE);
    this->publicchat = chat->publicChat();
    this->unifiedKey = chat->getUnifiedKey();
    this->meeting = chat->getMeeting();
    this->chatOptions = chat->getChatOptions();
    this->changed = 0;

    if (!chat->getSchedMeetings().empty())
    {
        mScheduledMeetings.reset(MegaScheduledMeetingList::createInstance());
        for (auto it = chat->getSchedMeetings().begin(); it != chat->getSchedMeetings().end(); it++)
        {
            mScheduledMeetings->insert(new MegaScheduledMeetingPrivate(it->second.get()));
        }
    }

    if (!chat->getSchedMeetingsChanged().empty())
    {
        mSchedMeetingsChanged.reset(MegaHandleList::createInstance());
        for (auto it = chat->getSchedMeetingsChanged().begin(); it != chat->getSchedMeetingsChanged().end(); it++)
        {
            mSchedMeetingsChanged->addMegaHandle(*it);
        }
        changed |= MegaTextChat::CHANGE_TYPE_SCHED_MEETING;
    }

    if (chat->changed.schedOcurrReplace || chat->changed.schedOcurrAppend)
    {
        changed |= chat->changed.schedOcurrReplace
                ? MegaTextChat::CHANGE_TYPE_SCHED_REPLACE_OCURR
                : MegaTextChat::CHANGE_TYPE_SCHED_APPEND_OCURR;

        if (!chat->getUpdatedOcurrences().empty())
        {
            mUpdatedOcurrences.reset(MegaScheduledMeetingList::createInstance());
            for (auto it = chat->getUpdatedOcurrences().begin(); it != chat->getUpdatedOcurrences().end(); it++)
            {
                mUpdatedOcurrences->insert(new MegaScheduledMeetingPrivate((*it).get()));
            }
        }
    }

    if (chat->changed.attachments)
    {
        changed |= MegaTextChat::CHANGE_TYPE_ATTACHMENT;
    }
    if (chat->changed.flags)
    {
        changed |= MegaTextChat::CHANGE_TYPE_FLAGS;
    }
    if (chat->changed.mode)
    {
        changed |= MegaTextChat::CHANGE_TYPE_MODE;
    }
    if (chat->changed.options)
    {
        changed |= MegaTextChat::CHANGE_TYPE_CHAT_OPTIONS;
    }
}

MegaTextChat *MegaTextChatPrivate::copy() const
{
    return new MegaTextChatPrivate(this);
}

MegaTextChatPrivate::~MegaTextChatPrivate()
{
    delete peers;
}

MegaHandle MegaTextChatPrivate::getHandle() const
{
    return id;
}

int MegaTextChatPrivate::getOwnPrivilege() const
{
    return priv;
}

int MegaTextChatPrivate::getShard() const
{
    return shard;
}

const MegaTextChatPeerList *MegaTextChatPrivate::getPeerList() const
{
    return peers;
}

void MegaTextChatPrivate::setPeerList(const MegaTextChatPeerList* newPeers)
{
    if (peers)
    {
        delete peers;
    }
    peers = newPeers ? newPeers->copy() : nullptr;
}

bool MegaTextChatPrivate::isGroup() const
{
    return group;
}

MegaHandle MegaTextChatPrivate::getOriginatingUser() const
{
    return ou;
}

const char *MegaTextChatPrivate::getTitle() const
{
    return !title.empty() ? title.c_str() : NULL;
}

const char *MegaTextChatPrivate::getUnifiedKey() const
{
    return !unifiedKey.empty() ? unifiedKey.c_str() : NULL;
}

unsigned char MegaTextChatPrivate::getChatOptions() const
{
    return chatOptions;
}

int MegaTextChatPrivate::isOwnChange() const
{
    return tag;
}

int64_t MegaTextChatPrivate::getCreationTime() const
{
    return ts;
}

bool MegaTextChatPrivate::isArchived() const
{
    return archived;
}

bool MegaTextChatPrivate::isPublicChat() const
{
    return publicchat;
}

bool MegaTextChatPrivate::isMeeting() const
{
    return meeting;
}

bool MegaTextChatPrivate::hasChanged(uint64_t changeType) const
{
    return (changed & changeType);
}

uint64_t MegaTextChatPrivate::getChanges() const
{
    return changed;
}

const MegaScheduledMeetingList* MegaTextChatPrivate::getScheduledMeetingList() const
{
    return mScheduledMeetings.get();
}

const MegaScheduledMeetingList* MegaTextChatPrivate::getUpdatedOccurrencesList() const
{
    return mUpdatedOcurrences.get();
}

const MegaHandleList* MegaTextChatPrivate::getSchedMeetingsChanged() const
{
    return mSchedMeetingsChanged.get();
}

MegaTextChatListPrivate::~MegaTextChatListPrivate()
{
    for (unsigned int i = 0; i < (unsigned int) size(); i++)
    {
        delete list.at(i);
    }
}

MegaTextChatList *MegaTextChatListPrivate::copy() const
{
    return new MegaTextChatListPrivate(this);
}

const MegaTextChat *MegaTextChatListPrivate::get(unsigned int i) const
{
    if (i >= (unsigned int) size())
    {
        return NULL;
    }
    else
    {
        return list.at(i);
    }
}

int MegaTextChatListPrivate::size() const
{
    return int(list.size());
}

void MegaTextChatListPrivate::addChat(MegaTextChatPrivate *chat)
{
    list.push_back(chat);
}

MegaTextChatListPrivate::MegaTextChatListPrivate(const MegaTextChatListPrivate *list)
{
    MegaTextChatPrivate *chat;

    for (unsigned int i = 0; i < (unsigned int) list->size(); i++)
    {
        chat = new MegaTextChatPrivate(list->get(i));
        this->list.push_back(chat);
    }
}

MegaTextChatListPrivate::MegaTextChatListPrivate()
{

}

MegaTextChatListPrivate::MegaTextChatListPrivate(textchat_map *list)
{
    MegaTextChatPrivate *megaChat;

    textchat_map::iterator it;
    for (it = list->begin(); it != list->end(); it++)
    {
        megaChat = new MegaTextChatPrivate(it->second);
        this->list.push_back(megaChat);
    }
}

MegaScheduledFlagsPrivate::MegaScheduledFlagsPrivate()
    : mScheduledFlags(std::make_unique<ScheduledFlags>())
{}

MegaScheduledFlagsPrivate::MegaScheduledFlagsPrivate(const unsigned long numericValue)
    : mScheduledFlags(std::make_unique<ScheduledFlags>(numericValue))
{}

MegaScheduledFlagsPrivate::MegaScheduledFlagsPrivate(const MegaScheduledFlagsPrivate *flags)
    : mScheduledFlags(std::make_unique<ScheduledFlags>(flags ? flags->getNumericValue() : ScheduledFlags::schedEmptyFlags))
{}

MegaScheduledFlagsPrivate::MegaScheduledFlagsPrivate(const ScheduledFlags* flags)
    : mScheduledFlags(std::make_unique<ScheduledFlags>(flags ? flags->getNumericValue() : ScheduledFlags::schedEmptyFlags))
{}

void MegaScheduledFlagsPrivate::reset()                             { mScheduledFlags->reset(); }
void MegaScheduledFlagsPrivate::setSendEmails(bool enabled)         { mScheduledFlags->setSendEmails(enabled); }
void MegaScheduledFlagsPrivate::importFlagsValue(unsigned long val) { mScheduledFlags->importFlagsValue(val); }

bool MegaScheduledFlagsPrivate::sendEmails() const                  { return mScheduledFlags->sendEmails(); }
unsigned long MegaScheduledFlagsPrivate::getNumericValue() const    { return mScheduledFlags->getNumericValue();}

bool MegaScheduledFlagsPrivate::isEmpty() const                     { return mScheduledFlags->isEmpty(); }
unique_ptr<ScheduledFlags> MegaScheduledFlagsPrivate::getSdkScheduledFlags() const
{
    return std::make_unique<ScheduledFlags>(getNumericValue());
}

MegaScheduledRulesPrivate::MegaScheduledRulesPrivate(const int freq, const int interval, const MegaTimeStamp until,
                                                     const MegaIntegerList* byWeekDay, const MegaIntegerList* byMonthDay,
                                                     const MegaIntegerMap* byMonthWeekDay)
    : mScheduledRules(std::make_unique<ScheduledRules>(
                          isValidFreq(freq) ? freq : FREQ_INVALID,
                          isValidInterval(interval) ? interval : int{INTERVAL_INVALID},
                          //                                     ^^^^^^^^^^^^^^^^^^^^^
                          // int workaroud due to ODR-use requirement in C++11 (dropped from C++17)
                          isValidUntil(until) ? until : MEGA_INVALID_TIMESTAMP,
                          byWeekDay ? unique_ptr<MegaSmallIntVector>(
                                  dynamic_cast<const MegaIntegerListPrivate*>(byWeekDay)->toByteList()
                                  ).get()
                              : nullptr,
                          byMonthDay ? unique_ptr<MegaSmallIntVector>(
                                  dynamic_cast<const MegaIntegerListPrivate*>(byMonthDay)->toByteList()
                                  ).get()
                              :nullptr,
                          byMonthWeekDay ? unique_ptr<MegaSmallIntMap>(
                                  dynamic_cast<const MegaIntegerMapPrivate*>(byMonthWeekDay)->toByteMap()
                                  ).get()
                              : nullptr))
{}

MegaScheduledRulesPrivate::MegaScheduledRulesPrivate(const MegaScheduledRulesPrivate *rules)
    : mScheduledRules(rules->getSdkScheduledRules())
{}

MegaScheduledRulesPrivate::MegaScheduledRulesPrivate(const ScheduledRules *rules)
    : mScheduledRules(rules->copy())
{}

int MegaScheduledRulesPrivate::freq() const            { return mScheduledRules->freq(); }
int MegaScheduledRulesPrivate::interval() const        { return mScheduledRules->interval(); }
MegaTimeStamp MegaScheduledRulesPrivate::until() const { return mScheduledRules->until(); }

/*
 * Data cannot be changed through the public interface and is only set via ctor. We abuse the
 * choke point of the following getters to set it instead of the various ctors.
 *
 * Probable redesign of MegaApi will end up with returning ownership of these pointers
 * and the temporal variables won't be needed
 */
const MegaIntegerList* MegaScheduledRulesPrivate::byWeekDay() const
{
    const auto p = mScheduledRules->byWeekDay();
    if (!mTransformedByWeekDay && p)
    {
        mTransformedByWeekDay = std::make_unique<MegaIntegerListPrivate>(*p);
    }

    return mTransformedByWeekDay.get();
}

const MegaIntegerList* MegaScheduledRulesPrivate::byMonthDay() const
{
    const auto p = mScheduledRules->byMonthDay();
    if (!mTransformedByMonthDay && p)
    {
        mTransformedByMonthDay.reset(new MegaIntegerListPrivate(*p));
    }

    return mTransformedByMonthDay.get();
}

const MegaIntegerMap* MegaScheduledRulesPrivate::byMonthWeekDay() const
{
    const auto p = mScheduledRules->byMonthWeekDay();
    if (!mTransformedByMonthWeekDay && p)
    {
        mTransformedByMonthWeekDay.reset(new MegaIntegerMapPrivate(*p));
    }

    return mTransformedByMonthWeekDay.get();
}

unique_ptr<ScheduledRules> MegaScheduledRulesPrivate::getSdkScheduledRules() const
{
    return unique_ptr<ScheduledRules>(mScheduledRules->copy());
}

bool MegaScheduledRulesPrivate::isValidFreq(const int freq)         { return ScheduledRules::isValidFreq(freq); }
bool MegaScheduledRulesPrivate::isValidInterval(const int interval) { return ScheduledRules::isValidInterval(interval); }
bool MegaScheduledRulesPrivate::isValidUntil(const m_time_t until)  { return ScheduledRules::isValidUntil(until); }

MegaScheduledMeetingPrivate::MegaScheduledMeetingPrivate(const MegaHandle chatid, const char* timezone,
                                                         const MegaTimeStamp startDateTime, const MegaTimeStamp endDateTime,
                                                         const char* title, const char* description, MegaHandle schedId,
                                                         MegaHandle parentSchedId, MegaHandle organizerUserId, int cancelled,
                                                         const char* attributes, MegaTimeStamp overrides,
                                                         const MegaScheduledFlags* flags, const MegaScheduledRules* rules)
    : mScheduledMeeting(std::make_unique<ScheduledMeeting>(
                            chatid,
                            timezone ? timezone : std::string(),
                            startDateTime, endDateTime,
                            title ? title : std::string(),
                            description ? description : std::string(),
                            organizerUserId, schedId, parentSchedId, cancelled,
                            attributes ? attributes : std::string(),
                            overrides,
                            flags ? dynamic_cast<const MegaScheduledFlagsPrivate*>(flags)->getSdkScheduledFlags().get() : nullptr,
                            rules ? dynamic_cast<const MegaScheduledRulesPrivate*>(rules)->getSdkScheduledRules().get() : nullptr))
{}

MegaScheduledMeetingPrivate::MegaScheduledMeetingPrivate(const MegaScheduledMeetingPrivate* sm)
    : mScheduledMeeting(sm->scheduledMeeting()->copy())
{}

MegaScheduledMeetingPrivate::MegaScheduledMeetingPrivate(const ScheduledMeeting *scheduledMeeting)
    : mScheduledMeeting(scheduledMeeting->copy())
{}

MegaHandle MegaScheduledMeetingPrivate::chatid() const           { return mScheduledMeeting->chatid(); }
MegaHandle MegaScheduledMeetingPrivate::schedId() const          { return mScheduledMeeting->schedId(); }
MegaHandle MegaScheduledMeetingPrivate::parentSchedId() const    { return mScheduledMeeting->parentSchedId(); }
MegaHandle MegaScheduledMeetingPrivate::organizerUserid() const  { return mScheduledMeeting->organizerUserid(); }
const char* MegaScheduledMeetingPrivate::timezone() const        { return mScheduledMeeting->timezone().size() ? mScheduledMeeting->timezone().c_str() : nullptr; }
MegaTimeStamp MegaScheduledMeetingPrivate::startDateTime() const { return mScheduledMeeting->startDateTime(); }
MegaTimeStamp MegaScheduledMeetingPrivate::endDateTime() const   { return mScheduledMeeting->endDateTime(); }
const char* MegaScheduledMeetingPrivate::title() const           { return mScheduledMeeting->title().size() ? mScheduledMeeting->title().c_str() : nullptr; }
const char* MegaScheduledMeetingPrivate::description() const     { return mScheduledMeeting->description().size() ? mScheduledMeeting->description().c_str() : nullptr; }
const char* MegaScheduledMeetingPrivate::attributes() const      { return mScheduledMeeting->attributes().size() ? mScheduledMeeting->attributes().c_str() : nullptr; }
MegaTimeStamp MegaScheduledMeetingPrivate::overrides() const     { return mScheduledMeeting->overrides(); }
int MegaScheduledMeetingPrivate::cancelled() const               { return mScheduledMeeting->cancelled(); }
MegaScheduledFlags* MegaScheduledMeetingPrivate::flags() const   { return mScheduledMeeting->flags() ? new MegaScheduledFlagsPrivate(mScheduledMeeting->flags()) : nullptr;}
MegaScheduledRules* MegaScheduledMeetingPrivate::rules() const   { return mScheduledMeeting->rules() ? new MegaScheduledRulesPrivate(mScheduledMeeting->rules()) : nullptr;}

MegaScheduledMeetingListPrivate::MegaScheduledMeetingListPrivate()
{
}

MegaScheduledMeetingListPrivate::MegaScheduledMeetingListPrivate(const MegaScheduledMeetingListPrivate& l)
{
    mList.reserve(l.size());
    for (unsigned long i = 0; i < l.size(); i++)
    {
        mList.emplace_back(l.at(i)->copy());
    }
}

MegaScheduledMeetingListPrivate::~MegaScheduledMeetingListPrivate()
{
    // all objects managed by unique_ptr's containted in mList will be deallocated when mList is destroyed
}

unsigned long MegaScheduledMeetingListPrivate::size() const
{
    return static_cast<unsigned long>(mList.size());
}

MegaScheduledMeetingListPrivate* MegaScheduledMeetingListPrivate::copy() const
{
   return new MegaScheduledMeetingListPrivate(*this);
}

MegaScheduledMeeting* MegaScheduledMeetingListPrivate::at(unsigned long i) const
{
    return mList.at(i).get();
}

MegaScheduledMeeting* MegaScheduledMeetingListPrivate::getBySchedId(handle h) const
{
    auto it = std::find_if(mList.begin(),
                   mList.end(),
                   [h](const std::unique_ptr<MegaScheduledMeeting>& sm) -> bool
                   {
                       return h == sm ->schedId();
                   });

    return (it != mList.end())
        ? it->get()
        : nullptr;
}

void MegaScheduledMeetingListPrivate::insert(MegaScheduledMeeting* sm)
{
    mList.emplace_back(sm);
}

void MegaScheduledMeetingListPrivate::clear()
{
     mList.clear();
}
#endif

MegaTransferDataPrivate::MegaTransferDataPrivate(TransferList *transferList, long long notificationNumber)
{
    numDownloads = int(transferList->transfers[GET].size());
    downloadTags.reserve(static_cast<size_t>(numDownloads));
    downloadPriorities.reserve(static_cast<size_t>(numDownloads));
    for (auto it = transferList->begin(GET); it != transferList->end(GET); it++)
    {
        Transfer *transfer = (*it);
        for (file_list::iterator fit = transfer->files.begin(); fit != transfer->files.end(); fit++)
        {
            File *file = (*fit);
            downloadTags.push_back(file->tag);
            downloadPriorities.push_back(transfer->priority);
        }
    }
    numDownloads = int(downloadTags.size());

    numUploads = int(transferList->transfers[PUT].size());
    uploadTags.reserve(static_cast<size_t>(numUploads));
    uploadPriorities.reserve(static_cast<size_t>(numUploads));
    for (auto it = transferList->begin(PUT); it != transferList->end(PUT); it++)
    {
        Transfer *transfer = (*it);
        for (file_list::iterator fit = transfer->files.begin(); fit != transfer->files.end(); fit++)
        {
            File *file = (*fit);
            uploadTags.push_back(file->tag);
            uploadPriorities.push_back(transfer->priority);
        }
    }
    numUploads = int(uploadTags.size());

    this->notificationNumber = notificationNumber;
}

MegaTransferDataPrivate::MegaTransferDataPrivate(const MegaTransferDataPrivate *transferData)
{
    this->numDownloads = transferData->numDownloads;
    this->numUploads = transferData->numUploads;
    this->downloadTags = transferData->downloadTags;
    this->uploadTags = transferData->uploadTags;
    this->downloadPriorities = transferData->downloadPriorities;
    this->uploadPriorities = transferData->uploadPriorities;
    this->notificationNumber = transferData->notificationNumber;
}

MegaTransferDataPrivate::~MegaTransferDataPrivate()
{

}

MegaTransferData *MegaTransferDataPrivate::copy() const
{
    return new MegaTransferDataPrivate(this);
}

int MegaTransferDataPrivate::getNumDownloads() const
{
    return numDownloads;
}

int MegaTransferDataPrivate::getNumUploads() const
{
    return numUploads;
}

int MegaTransferDataPrivate::getDownloadTag(int i) const
{
    return downloadTags[static_cast<size_t>(i)];
}

int MegaTransferDataPrivate::getUploadTag(int i) const
{
    return uploadTags[static_cast<size_t>(i)];
}

unsigned long long MegaTransferDataPrivate::getDownloadPriority(int i) const
{
    return downloadPriorities[static_cast<size_t>(i)];
}

unsigned long long MegaTransferDataPrivate::getUploadPriority(int i) const
{
    return uploadPriorities[static_cast<size_t>(i)];
}

long long MegaTransferDataPrivate::getNotificationNumber() const
{
    return notificationNumber;
}

MegaSizeProcessor::MegaSizeProcessor()
{
    totalBytes = 0;
}

bool MegaSizeProcessor::processMegaNode(MegaNode *node)
{
    if (node->getType() == MegaNode::TYPE_FILE)
    {
        totalBytes += node->getSize();
    }
    return true;
}

long long MegaSizeProcessor::getTotalBytes()
{
    return totalBytes;
}

MegaEventPrivate::MegaEventPrivate(int atype)
    :type(atype)
{
}

MegaEventPrivate::MegaEventPrivate(MegaEventPrivate *event)
{
    this->type = event->getType();
    this->setText(event->getText());
    this->setNumber(event->getNumber());
    this->numberMap = event->numberMap;
    this->setHandle(event->getHandle());
    mIntegerList.reset(event->mIntegerList ? event->mIntegerList->copy() : nullptr);
}

MegaEventPrivate::~MegaEventPrivate()
{
    delete[] text;
}

MegaEvent *MegaEventPrivate::copy()
{
    return new MegaEventPrivate(this);
}

int MegaEventPrivate::getType() const
{
    return type;
}

const char *MegaEventPrivate::getText() const
{
    return text;
}

int64_t MegaEventPrivate::getNumber() const
{
    return number;
}

void MegaEventPrivate::setText(const char* newText)
{
    if (text)
    {
        delete[] text;
    }
    text = MegaApi::strdup(newText);
}

void MegaEventPrivate::setNumber(int64_t newNumber)
{
    number = newNumber;
}

int64_t MegaEventPrivate::getNumber(const std::string& key) const
{
    if (auto it = numberMap.find(key); it != numberMap.end())
    {
        return it->second;
    }
    return 0;
}

bool MegaEventPrivate::hasNumber(const std::string& key) const
{
    return numberMap.find(key) != numberMap.end();
}

void MegaEventPrivate::setNumber(const std::string& key, int64_t value)
{
    numberMap[key] = value;
}

MegaHandle MegaEventPrivate::getHandle() const
{
    return mHandle;
}

MegaIntegerList* MegaEventPrivate::getIntegerList() const
{
    return mIntegerList.get();
}

void MegaEventPrivate::setIntegerList(MegaIntegerList* integerList)
{
    mIntegerList.reset(integerList);
}

const char *MegaEventPrivate::getEventString() const
{
    return MegaEventPrivate::getEventString(type);
}

const char *MegaEventPrivate::getEventString(int type)
{
    switch (type)
    {
        case MegaEvent::EVENT_COMMIT_DB: return "EVENT_COMMIT_DB";
        case MegaEvent::EVENT_ACCOUNT_CONFIRMATION: return "EVENT_ACCOUNT_CONFIRMATION";
        case MegaEvent::EVENT_CONFIRM_USER_EMAIL:
            return "EVENT_CONFIRM_USER_EMAIL";
        case MegaEvent::EVENT_DISCONNECT: return "EVENT_DISCONNECT";
        case MegaEvent::EVENT_ACCOUNT_BLOCKED: return "EVENT_ACCOUNT_BLOCKED";
        case MegaEvent::EVENT_STORAGE: return "EVENT_STORAGE";
        case MegaEvent::EVENT_NODES_CURRENT: return "EVENT_NODES_CURRENT";
        case MegaEvent::EVENT_MEDIA_INFO_READY: return "EVENT_MEDIA_INFO_READY";
        case MegaEvent::EVENT_STORAGE_SUM_CHANGED: return "EVENT_STORAGE_SUM_CHANGED";
        case MegaEvent::EVENT_BUSINESS_STATUS: return "BUSINESS_STATUS";
        case MegaEvent::EVENT_KEY_MODIFIED: return "KEY_MODIFIED";
        case MegaEvent::EVENT_MISC_FLAGS_READY: return "MISC_FLAGS_READY";
#ifdef ENABLE_SYNC
        case MegaEvent::EVENT_SYNCS_DISABLED: return "SYNCS_DISABLED";
        case MegaEvent::EVENT_SYNCS_RESTORED: return "SYNCS_RESTORED";
#endif
        case MegaEvent::EVENT_REQSTAT_PROGRESS: return "REQSTAT_PROGRESS";
        case MegaEvent::EVENT_RELOADING: return "RELOADING";
        case MegaEvent::EVENT_FATAL_ERROR: return "FATAL_ERROR";
        case MegaEvent::EVENT_UPGRADE_SECURITY:
            return "UPGRADE_SECURITY";
        case MegaEvent::EVENT_DOWNGRADE_ATTACK:
            return "DOWNGRADE_ATTACK";
        case MegaEvent::EVENT_CREDIT_CARD_EXPIRY:
            return "CREDIT_CARD_EXPIRY";
        case MegaEvent::EVENT_NETWORK_ACTIVITY:
            return "NETWORK_ACTIVITY";
        case MegaEvent::EVENT_TRANSFERS_RESUMED:
            return "TRANSFERS_RESUMED";
        case MegaEvent::EVENT_LAST_PURGE:
            return "LAST_PURGE";
        case MegaEvent::EVENT_STREAM_OVERQUOTA:
            return "STREAM_OVERQUOTA";
    }

    return "UNKNOWN";
}

std::string MegaEventPrivate::getValidDataToString() const
{
    std::string msg;
    if (getText())
    {
        msg.append(" text: ").append(getText());
    }

    if (getNumber() >= 0)
    {
        msg.append(" number: ").append(std::to_string(getNumber()));
    }

    if (getHandle() != INVALID_HANDLE)
    {
        msg.append(" handle: ").append(Base64Str<sizeof(MegaHandle)>(getHandle()));
    }
    return msg;
}

void MegaEventPrivate::setHandle(const MegaHandle &handle)
{
    mHandle = handle;
}

MegaHandleListPrivate::MegaHandleListPrivate()
{

}

MegaHandleListPrivate::MegaHandleListPrivate(const MegaHandleListPrivate *hList)
{
    mList = hList->mList;
}

MegaHandleListPrivate::MegaHandleListPrivate(const vector<handle> &handles)
{
    mList = handles;
}

MegaHandleListPrivate::~MegaHandleListPrivate()
{

}

MegaHandleList *MegaHandleListPrivate::copy() const
{
    return new MegaHandleListPrivate(this);
}

MegaHandle MegaHandleListPrivate::get(unsigned int i) const
{
    MegaHandle h = INVALID_HANDLE;

    if (i < mList.size())
    {
        h = mList.at(i);
    }

    return h;
}

unsigned int MegaHandleListPrivate::size() const
{
    return unsigned(mList.size());
}

void MegaHandleListPrivate::addMegaHandle(MegaHandle h)
{
    mList.push_back(h);
}

MegaIntegerListPrivate::MegaIntegerListPrivate(const vector<int8_t>& bytesList)
{
    mIntegers.reserve(bytesList.size());
    std::transform(bytesList.begin(), bytesList.end(), std::back_inserter(mIntegers), [](int8_t x) { return static_cast<int64_t>(x);});
}

MegaIntegerListPrivate::MegaIntegerListPrivate(const vector<int64_t>& integerList)
    : mIntegers(integerList)
{

}

MegaIntegerListPrivate::MegaIntegerListPrivate(const vector<uint32_t>& bytesList)
{
    mIntegers.reserve(bytesList.size());
    std::transform(bytesList.begin(), bytesList.end(), std::back_inserter(mIntegers), [](uint32_t x) { return static_cast<int64_t>(x);});
}

MegaIntegerListPrivate::MegaIntegerListPrivate()
{

}

MegaIntegerListPrivate::~MegaIntegerListPrivate()
{

}

MegaSmallIntVector* MegaIntegerListPrivate::toByteList() const
{
    MegaSmallIntVector* bytesList= new MegaSmallIntVector();
    bytesList->reserve(mIntegers.size());
    std::transform(mIntegers.begin(), mIntegers.end(), std::back_inserter(*bytesList), [](int64_t x) { return static_cast<int8_t>(x);});
    return bytesList;
}

MegaIntegerList* MegaIntegerListPrivate::copy() const
{
    return new MegaIntegerListPrivate(mIntegers);
}

int64_t MegaIntegerListPrivate::get(int i) const
{
    if (i >= static_cast<int>(mIntegers.size()))
    {
        return -1;
    }

    return mIntegers.at(static_cast<size_t>(i));
}

void MegaIntegerListPrivate::add(long long i)
{
    mIntegers.emplace_back(i);
}

int MegaIntegerListPrivate::size() const
{
    return static_cast<int>(mIntegers.size());
}

const vector<int64_t>* MegaIntegerListPrivate::getList() const
{
    return &mIntegers;
}

MegaChildrenListsPrivate::MegaChildrenListsPrivate(MegaChildrenLists *list)
    : folders(list->getFolderList()->copy())
    , files(list->getFileList()->copy())
{
}

MegaChildrenListsPrivate::MegaChildrenListsPrivate(unique_ptr<MegaNodeListPrivate> folderList, unique_ptr<MegaNodeListPrivate> fileList)
    : folders(std::move(folderList))
    , files(std::move(fileList))
{
}

MegaChildrenLists *MegaChildrenListsPrivate::copy()
{
    return new MegaChildrenListsPrivate(this);
}

MegaNodeList *MegaChildrenListsPrivate::getFileList()
{
    return files.get();
}

MegaNodeList *MegaChildrenListsPrivate::getFolderList()
{
    return folders.get();
}

MegaChildrenListsPrivate::MegaChildrenListsPrivate()
    : folders(new MegaNodeListPrivate())
    , files(new MegaNodeListPrivate())
{
}

MegaAchievementsDetails *MegaAchievementsDetailsPrivate::fromAchievementsDetails(AchievementsDetails *details)
{
    return new MegaAchievementsDetailsPrivate(details);
}

MegaAchievementsDetailsPrivate::~MegaAchievementsDetailsPrivate()
{ }

MegaAchievementsDetails *MegaAchievementsDetailsPrivate::copy()
{
    return new MegaAchievementsDetailsPrivate(&details);
}

long long MegaAchievementsDetailsPrivate::getBaseStorage()
{
    return details.permanent_size;
}

bool MegaAchievementsDetailsPrivate::isValidClass(int class_id)
{
    return details.achievements.find(static_cast<achievement_class_id>(class_id)) !=
           details.achievements.end();
}

long long MegaAchievementsDetailsPrivate::getClassStorage(int class_id)
{
    achievements_map::iterator it =
        details.achievements.find(static_cast<achievement_class_id>(class_id));
    if (it != details.achievements.end())
    {
        return it->second.storage;
    }

    return 0;
}

long long MegaAchievementsDetailsPrivate::getClassTransfer(int class_id)
{
    achievements_map::iterator it =
        details.achievements.find(static_cast<achievement_class_id>(class_id));
    if (it != details.achievements.end())
    {
        return it->second.transfer;
    }

    return 0;
}

int MegaAchievementsDetailsPrivate::getClassExpire(int class_id)
{
    achievements_map::iterator it =
        details.achievements.find(static_cast<achievement_class_id>(class_id));
    if (it != details.achievements.end())
    {
        return it->second.expire;
    }

    return 0;
}

unsigned int MegaAchievementsDetailsPrivate::getAwardsCount()
{
    return unsigned(details.awards.size());
}

int MegaAchievementsDetailsPrivate::getAwardClass(unsigned int index)
{
    if (index < details.awards.size())
    {
        return static_cast<int>(details.awards.at(index).achievement_class);
    }

    return 0;
}

int MegaAchievementsDetailsPrivate::getAwardId(unsigned int index)
{
    if (index < details.awards.size())
    {
        return details.awards.at(index).award_id;
    }

    return 0;
}

int64_t MegaAchievementsDetailsPrivate::getAwardTimestamp(unsigned int index)
{
    if (index < details.awards.size())
    {
        return details.awards.at(index).ts;
    }

    return 0;
}

int64_t MegaAchievementsDetailsPrivate::getAwardExpirationTs(unsigned int index)
{
    if (index < details.awards.size())
    {
        return details.awards.at(index).expire;
    }

    return 0;
}

MegaStringList *MegaAchievementsDetailsPrivate::getAwardEmails(unsigned int index)
{
    if (index < details.awards.size())
    {
        if (details.awards.at(index).achievement_class == MEGA_ACHIEVEMENT_INVITE)
        {
            string_vector data;
            vector<string>::iterator it = details.awards.at(index).emails_invited.begin();
            while (it != details.awards.at(index).emails_invited.end())
            {
                data.push_back(*it);
                it++;
            }
            return new MegaStringListPrivate(std::move(data));
        }
    }

    return new MegaStringListPrivate();
}

int MegaAchievementsDetailsPrivate::getRewardsCount()
{
    return int(details.rewards.size());
}

int MegaAchievementsDetailsPrivate::getRewardAwardId(unsigned int index)
{
    if (index < details.rewards.size())
    {
        return details.rewards.at(index).award_id;
    }

    return -1;
}

long long MegaAchievementsDetailsPrivate::getRewardStorage(unsigned int index)
{
    if (index < details.rewards.size())
    {
        return details.rewards.at(index).storage;
    }

    return 0;
}

long long MegaAchievementsDetailsPrivate::getRewardTransfer(unsigned int index)
{
    if (index < details.rewards.size())
    {
        return details.rewards.at(index).transfer;
    }

    return 0;
}

long long MegaAchievementsDetailsPrivate::getRewardStorageByAwardId(int award_id)
{
    for (vector<Reward>::iterator itr = details.rewards.begin(); itr != details.rewards.end(); itr++)
    {
        if (itr->award_id == award_id)
        {
            return itr->storage;
        }
    }

    return 0;
}

long long MegaAchievementsDetailsPrivate::getRewardTransferByAwardId(int award_id)
{
    for (vector<Reward>::iterator itr = details.rewards.begin(); itr != details.rewards.end(); itr++)
    {
        if (itr->award_id == award_id)
        {
            return itr->transfer;
        }
    }

    return 0;
}

int MegaAchievementsDetailsPrivate::getRewardExpire(unsigned int index)
{
    if (index < details.rewards.size())
    {
        return details.rewards.at(index).expire;
    }

    return 0;
}

long long MegaAchievementsDetailsPrivate::currentStorage()
{
    long long total = 0;
    m_time_t ts = m_time();

    for (vector<Award>::iterator it = details.awards.begin(); it != details.awards.end(); it++)
    {
        if (it->expire > ts || it->expire == 0)
        {
            for (vector<Reward>::iterator itr = details.rewards.begin(); itr != details.rewards.end(); itr++)
            {
                if (itr->award_id == it->award_id)
                {
                    total += itr->storage;
                }
            }
        }
    }

    return total;
}

long long MegaAchievementsDetailsPrivate::currentTransfer()
{
    long long total = 0;
    m_time_t ts = m_time();

    for (vector<Award>::iterator it = details.awards.begin(); it != details.awards.end(); it++)
    {
        if (it->expire > ts || it->expire == 0)
        {
            for (vector<Reward>::iterator itr = details.rewards.begin(); itr != details.rewards.end(); itr++)
            {
                if (itr->award_id == it->award_id)
                {
                    total += itr->transfer;
                }
            }
        }
    }

    return total;
}

long long MegaAchievementsDetailsPrivate::currentStorageReferrals()
{
    long long total = 0;
    m_time_t ts = m_time();

    for (vector<Award>::iterator it = details.awards.begin(); it != details.awards.end(); it++)
    {
        if ( (it->expire > ts) && (it->achievement_class == MEGA_ACHIEVEMENT_INVITE) )
        {
            for (vector<Reward>::iterator itr = details.rewards.begin(); itr != details.rewards.end(); itr++)
            {
                if (itr->award_id == it->award_id)
                {
                    total += itr->storage;
                }
            }
        }
    }

    return total;
}

long long MegaAchievementsDetailsPrivate::currentTransferReferrals()
{
    long long total = 0;
    m_time_t ts = m_time();

    for (vector<Award>::iterator it = details.awards.begin(); it != details.awards.end(); it++)
    {
        if ( (it->expire > ts) && (it->achievement_class == MEGA_ACHIEVEMENT_INVITE) )
        {
            for (vector<Reward>::iterator itr = details.rewards.begin(); itr != details.rewards.end(); itr++)
            {
                if (itr->award_id == it->award_id)
                {
                    total += itr->transfer;
                }
            }
        }
    }

    return total;
}

MegaAchievementsDetailsPrivate::MegaAchievementsDetailsPrivate(AchievementsDetails *details)
{
    this->details = (*details);
}

MegaFolderInfoPrivate::MegaFolderInfoPrivate(int numFiles, int numFolders, int numVersions, long long currentSize, long long versionsSize)
{
    this->numFiles = numFiles;
    this->numFolders = numFolders;
    this->numVersions = numVersions;
    this->currentSize = currentSize;
    this->versionsSize = versionsSize;
}

MegaFolderInfoPrivate::MegaFolderInfoPrivate(const MegaFolderInfoPrivate *folderData)
{
    this->numFiles = folderData->getNumFiles();
    this->numFolders = folderData->getNumFolders();
    this->numVersions = folderData->getNumVersions();
    this->currentSize = folderData->getCurrentSize();
    this->versionsSize = folderData->getVersionsSize();
}

MegaFolderInfoPrivate::~MegaFolderInfoPrivate()
{

}

MegaFolderInfo *MegaFolderInfoPrivate::copy() const
{
    return new MegaFolderInfoPrivate(this);
}

int MegaFolderInfoPrivate::getNumVersions() const
{
    return numVersions;
}

int MegaFolderInfoPrivate::getNumFiles() const
{
    return numFiles;
}

int MegaFolderInfoPrivate::getNumFolders() const
{
    return numFolders;
}

long long MegaFolderInfoPrivate::getCurrentSize() const
{
    return currentSize;
}

long long MegaFolderInfoPrivate::getVersionsSize() const
{
    return versionsSize;
}

MegaTimeZoneDetailsPrivate::MegaTimeZoneDetailsPrivate(vector<std::string> *timeZones, vector<int> *timeZoneOffsets, int defaultTimeZone)
{
    this->timeZones = *timeZones;
    this->timeZoneOffsets = *timeZoneOffsets;
    this->defaultTimeZone = defaultTimeZone;
}

MegaTimeZoneDetailsPrivate::MegaTimeZoneDetailsPrivate(const MegaTimeZoneDetailsPrivate *timeZoneDetails)
{
    this->timeZones = timeZoneDetails->timeZones;
    this->timeZoneOffsets = timeZoneDetails->timeZoneOffsets;
    this->defaultTimeZone = timeZoneDetails->defaultTimeZone;
}

MegaTimeZoneDetailsPrivate::~MegaTimeZoneDetailsPrivate()
{

}

MegaTimeZoneDetails *MegaTimeZoneDetailsPrivate::copy() const
{
    return new MegaTimeZoneDetailsPrivate(this);
}

int MegaTimeZoneDetailsPrivate::getNumTimeZones() const
{
    return int(timeZones.size());
}

const char *MegaTimeZoneDetailsPrivate::getTimeZone(int index) const
{
    if (index >= 0 && index < int(timeZones.size()))
    {
        return timeZones[static_cast<size_t>(index)].c_str();
    }
    return "";
}

int MegaTimeZoneDetailsPrivate::getTimeOffset(int index) const
{
    if (index >= 0 && index < int(timeZoneOffsets.size()))
    {
        return timeZoneOffsets[static_cast<size_t>(index)];
    }
    return 0;
}

int MegaTimeZoneDetailsPrivate::getDefault() const
{
    return defaultTimeZone;
}

MegaPushNotificationSettingsPrivate::MegaPushNotificationSettingsPrivate(const string &settingsJSON)
{
    JSON json;
    json.begin(settingsJSON.c_str() + 1);
    std::string name = json.getname();
    while (!name.empty())
    {
        std::string globalObject;
        json.storeobject(&globalObject);
        if (name == "GLOBAL")
        {
            JSON jsonGlobal;
            jsonGlobal.begin(globalObject.c_str() + 1);
            std::string globalSubsetting = jsonGlobal.getname();
            while (!globalSubsetting.empty())
            {
                if (globalSubsetting == "dnd")
                {
                    if (jsonGlobal.isnumeric())
                    {
                        mGlobalDND = jsonGlobal.getint();
                    }

                    if (mGlobalDND < 0)
                    {
                        LOG_err << "Invalid format in GLOBAL.dnd notification settings";
                        mJsonInvalid = true;
                    }
                }
                else if (globalSubsetting == "nsch")
                {
                    std::string schedule;
                    jsonGlobal.storeobject(&schedule);
                    JSON scheduleJson;
                    scheduleJson.begin(schedule.c_str() + 1);
                    std::string scheduleSubTitle = scheduleJson.getname();
                    while (!scheduleSubTitle.empty())
                    {
                        if (scheduleSubTitle == "tz")
                        {
                            scheduleJson.storeobject(&mGlobalScheduleTimezone);
                        }
                        else if (scheduleSubTitle == "start" && scheduleJson.isnumeric())
                        {
                            mGlobalScheduleStart = scheduleJson.getint32();
                        }
                        else if (scheduleSubTitle == "end" && scheduleJson.isnumeric())
                        {
                            mGlobalScheduleEnd = scheduleJson.getint32();
                        }
                        else
                        {
                            LOG_warn << "Unknown option in GLOBAL.nsch notification settings";
                        }

                        scheduleSubTitle = scheduleJson.getname();
                    }

                    if (mGlobalScheduleTimezone.empty() || mGlobalScheduleStart < 0 || mGlobalScheduleEnd < 0)
                    {
                        LOG_err << "Invalid format in GLOBAL.nsch notification settings";
                        mJsonInvalid = true;
                    }
                }
                else
                {
                    LOG_warn << "Unknown option in Global notification settings";
                    mJsonInvalid = true;
                }

                globalSubsetting = jsonGlobal.getname();
            }
        }
        else if (name == "PCR")
        {
            JSON jsonContact;
            jsonContact.begin(globalObject.c_str() + 1);
            std::string subname = jsonContact.getname();
            if (subname != "dnd")
            {
                LOG_warn << "Unknown option in PCR notification settings";
                mJsonInvalid = true;
            }
            else if (jsonContact.isnumeric())
            {
                mContactsDND = jsonContact.getint();
            }
            else
            {
                LOG_err << "Invalid format in PCR.dnd notification settings";
                mJsonInvalid = true;
            }
        }
        else if (name == "INSHARE")
        {
            JSON jsonShares;
            jsonShares.begin(globalObject.c_str() + 1);
            std::string subname = jsonShares.getname();
            if (subname != "dnd")
            {
                LOG_warn << "Unknown option in SHARES notification settings";
                mJsonInvalid = true;
            }
            else if (jsonShares.isnumeric())
            {
                mSharesDND = jsonShares.getint();
            }
            else
            {
                LOG_err << "Invalid format in SHARES.dnd notification settings";
                mJsonInvalid = true;
            }
        }
        else if (name == "CHAT")
        {
            JSON jsonChats;
            jsonChats.begin(globalObject.c_str() + 1);
            std::string subname = jsonChats.getname();
            if (subname != "dnd")
            {
                LOG_warn << "Unknown option in CHAT notification settings";
                mJsonInvalid = true;
            }
            else if (jsonChats.isnumeric())
            {
                mGlobalChatsDND = jsonChats.getint();
            }
            else
            {
                LOG_err << "Invalid format in CHAT.dnd notification settings";
                mJsonInvalid = true;
            }
        }
        else if (name.length() == 11)    // settings for specific 'chatid'
        {
            MegaHandle chatid;
            if (Base64::atob(name.c_str(), (byte*)&chatid, MegaClient::CHATHANDLE) == MegaClient::CHATHANDLE)
            {
                JSON jsonChat;
                jsonChat.begin(globalObject.c_str() + 1);
                std::string subname = jsonChat.getname();
                if (subname == "dnd" && jsonChat.isnumeric())
                {
                    mChatDND[chatid] = jsonChat.getint();
                }
                else if (subname == "an" && jsonChat.isnumeric())
                {
                    bool alwaysNotify = jsonChat.getbool();
                    if (alwaysNotify)
                    {
                        mChatAlwaysNotify[chatid] = true;
                    }
                }
                else
                {
                    LOG_err << "Unknown option for chatid '" << name << "' in notification settings";
                    mJsonInvalid = true;
                }
            }
            else
            {
                LOG_warn << "Invalid format for chatid '" << name << "' in notification settings";
                mJsonInvalid = true;
            }
        }
        else
        {
            LOG_err << "Unknown option in notification settings";
            mJsonInvalid = true;
        }

        if (mJsonInvalid)
        {
            return;
        }

        name = json.getname();
    }
}

MegaPushNotificationSettingsPrivate::MegaPushNotificationSettingsPrivate()
{
}

MegaPushNotificationSettingsPrivate::MegaPushNotificationSettingsPrivate(const MegaPushNotificationSettingsPrivate *settings)
{
    mGlobalDND = settings->mGlobalDND;
    mGlobalScheduleStart = settings->mGlobalScheduleStart;
    mGlobalScheduleEnd = settings->mGlobalScheduleEnd;
    mGlobalScheduleTimezone = settings->mGlobalScheduleTimezone;
    mChatDND = settings->mChatDND;
    mChatAlwaysNotify = settings->mChatAlwaysNotify;
    mContactsDND = settings->mContactsDND;
    mSharesDND = settings->mSharesDND;
    mGlobalChatsDND = settings->mGlobalChatsDND;
}

bool MegaPushNotificationSettingsPrivate::operator==(
    const MegaPushNotificationSettingsPrivate& other) const
{
    return mGlobalDND == other.mGlobalDND && mGlobalScheduleStart == other.mGlobalScheduleStart &&
           mGlobalScheduleEnd == other.mGlobalScheduleEnd &&
           mGlobalScheduleTimezone == other.mGlobalScheduleTimezone && mChatDND == other.mChatDND &&
           mChatAlwaysNotify == other.mChatAlwaysNotify && mContactsDND == other.mContactsDND &&
           mSharesDND == other.mSharesDND && mGlobalChatsDND == other.mGlobalChatsDND &&
           mJsonInvalid == other.mJsonInvalid;
}

string MegaPushNotificationSettingsPrivate::generateJson() const
{
    std::string json;
    if ((mGlobalScheduleStart > -1 && mGlobalScheduleEnd == -1)
            || (mGlobalScheduleStart == -1 && mGlobalScheduleEnd > -1))
    {
        LOG_warn << "Invalid notification settings for GLOBAL.nsch";
        return json;
    }

    json = "{";
    if (mGlobalDND > -1 || isGlobalScheduleEnabled())
    {
        json.append("\"GLOBAL\":{");
        if (isGlobalDndEnabled())
        {
            json.append("\"dnd\":").append(std::to_string(mGlobalDND));
            json.append(",");
        }

        if (isGlobalScheduleEnabled())
        {
            json.append("\"nsch\":{\"start\":").append(std::to_string(mGlobalScheduleStart));
            json.append(",\"end\":").append(std::to_string(mGlobalScheduleEnd));
            json.append(",\"tz\":\"").append(mGlobalScheduleTimezone).append("\"}");
        }

        if (json.at(json.length() - 1) == ',')  // clear a tailing comma (not needed)
        {
            json.pop_back();
        }

        json.append("},");
    }

    if (mContactsDND > -1)
    {
        json.append("\"PCR\":{\"dnd\":").append(std::to_string(mContactsDND)).append("}");
        json.append(",");
    }

    if (mSharesDND > -1)
    {
        json.append("\"INSHARE\":{\"dnd\":").append(std::to_string(mSharesDND)).append("}");
        json.append(",");
    }

    if (isGlobalChatsDndEnabled())
    {
        json.append("\"CHAT\":{\"dnd\":").append(std::to_string(mGlobalChatsDND)).append("}");
        json.append(",");
    }

    char chatid[MegaClient::CHATHANDLE * 4 / 3 + 4];

    std::map<uint64_t, m_time_t>::const_iterator itDND;
    for (itDND = mChatDND.begin(); itDND != mChatDND.end(); itDND++)
    {
        if (!isChatAlwaysNotifyEnabled(itDND->first) && isChatDndEnabled(itDND->first))
        {
            Base64::btoa((byte*)&(itDND->first), MegaClient::CHATHANDLE, chatid);
            json.append("\"").append(chatid).append("\":{");
            json.append("\"dnd\":").append(std::to_string(itDND->second)).append("}");
            json.append(",");
        }
    }

    std::map<uint64_t, bool>::const_iterator itAn;
    for (itAn = mChatAlwaysNotify.begin(); itAn != mChatAlwaysNotify.end(); itAn++)
    {
        assert(isChatAlwaysNotifyEnabled(itAn->first));
        assert(!isChatDndEnabled(itAn->first));

        if (isChatAlwaysNotifyEnabled(itAn->first))
        {
            Base64::btoa((byte*)&(itAn->first), MegaClient::CHATHANDLE, chatid);
            json.append("\"").append(chatid).append("\":{");
            json.append("\"an\":").append("1").append("}");
            json.append(",");
        }
    }

    if (json.at(json.length() - 1) == ',')  // clear a tailing comma (not needed)
    {
        json.pop_back();
    }
    json.append("}");
    return json;
}

bool MegaPushNotificationSettingsPrivate::isValid() const
{
    return !mJsonInvalid;
}

MegaPushNotificationSettingsPrivate::~MegaPushNotificationSettingsPrivate()
{

}

bool MegaPushNotificationSettingsPrivate::isGlobalDndEnabled() const
{
    return (mGlobalDND == 0 || mGlobalDND > m_time(NULL));
}

bool MegaPushNotificationSettingsPrivate::isGlobalChatsDndEnabled() const
{
    return (mGlobalChatsDND == 0 || mGlobalChatsDND > m_time(NULL));
}

int64_t MegaPushNotificationSettingsPrivate::getGlobalDnd() const
{
    return mGlobalDND;
}

int64_t MegaPushNotificationSettingsPrivate::getGlobalChatsDnd() const
{
    return mGlobalChatsDND;
}

bool MegaPushNotificationSettingsPrivate::isGlobalScheduleEnabled() const
{
    return (mGlobalScheduleStart != -1 && mGlobalScheduleEnd != -1 && !mGlobalScheduleTimezone.empty());
}

int MegaPushNotificationSettingsPrivate::getGlobalScheduleStart() const
{
    return mGlobalScheduleStart;
}

int MegaPushNotificationSettingsPrivate::getGlobalScheduleEnd() const
{
    return mGlobalScheduleEnd;
}

const char *mega::MegaPushNotificationSettingsPrivate::getGlobalScheduleTimezone() const
{
    return MegaApi::strdup(mGlobalScheduleTimezone.c_str());
}

bool MegaPushNotificationSettingsPrivate::isChatDndEnabled(MegaHandle chatid) const
{
    std::map<uint64_t, m_time_t>::const_iterator it = mChatDND.find(chatid);
    return (it != mChatDND.end() && (it->second == 0 || it->second > m_time(NULL)));
}

int64_t MegaPushNotificationSettingsPrivate::getChatDnd(MegaHandle chatid) const
{
    std::map<uint64_t, m_time_t>::const_iterator it = mChatDND.find(chatid);
    if (it != mChatDND.end())
    {
        assert(it->second != -1);
        return it->second;
    }

    return -1;
}

bool MegaPushNotificationSettingsPrivate::isChatAlwaysNotifyEnabled(MegaHandle chatid) const
{
    std::map<uint64_t, bool>::const_iterator it = mChatAlwaysNotify.find(chatid);
    return (it != mChatAlwaysNotify.end() && it->second);
}

bool MegaPushNotificationSettingsPrivate::isContactsEnabled() const
{
    return (mContactsDND == -1 || (mContactsDND > 0 && mContactsDND < m_time(NULL)));
}

bool MegaPushNotificationSettingsPrivate::isSharesEnabled() const
{
    return (mSharesDND == -1 || (mSharesDND > 0 && mSharesDND < m_time(NULL)));
}

MegaPushNotificationSettings *MegaPushNotificationSettingsPrivate::copy() const
{
    return new MegaPushNotificationSettingsPrivate(this);
}

void MegaPushNotificationSettingsPrivate::enableGlobal(bool enable)
{
    if (!isGlobalDndEnabled() == enable)
    {
        return;
    }

    mGlobalDND = enable ? -1 : 0;
}

void MegaPushNotificationSettingsPrivate::setGlobalDnd(int64_t timestamp)
{
    assert(timestamp > 0);
    if (isGlobalDndEnabled())
    {
        LOG_warn << "setGlobalDnd(): global notifications are currently disabled."
                    " Setting a new time period for DND mode";
    }
    mGlobalDND = timestamp;
}

void MegaPushNotificationSettingsPrivate::disableGlobalDnd()
{
    if (isGlobalDndEnabled())
    {
        LOG_warn << "disableGlobalDnd(): global notifications were disabled. Now are enabled";
    }
    mGlobalDND = -1;
}

void MegaPushNotificationSettingsPrivate::setGlobalSchedule(int start, int end, const char *timezone)
{
    if (start <= -1 || end <= -1 || !timezone || !timezone[0] || start == end)
    {
        LOG_warn << "setGlobalSchedule(): wrong arguments";
        assert(false);
        return;
    }

    mGlobalScheduleStart = start;
    mGlobalScheduleEnd = end;
    mGlobalScheduleTimezone.assign(timezone);
}

void MegaPushNotificationSettingsPrivate::disableGlobalSchedule()
{
    mGlobalScheduleStart = -1;
    mGlobalScheduleEnd = -1;
    mGlobalScheduleTimezone.clear();
}

void MegaPushNotificationSettingsPrivate::enableChat(MegaHandle chatid, bool enable)
{
    assert(!ISUNDEF(chatid));
    if (!isChatDndEnabled(chatid) == enable)
    {
        return;
    }

    if (enable)
    {
        mChatDND.erase(chatid);
    }
    else    // disable
    {
        mChatDND[chatid] = 0;
        if (isChatAlwaysNotifyEnabled(chatid))
        {
            LOG_warn << "enableChat(): always notify was enabled. Now is disabled";
            enableChatAlwaysNotify(chatid, false);
        }
    }
}

void MegaPushNotificationSettingsPrivate::setChatDnd(MegaHandle chatid, int64_t timestamp)
{
    assert(timestamp > 0);
    assert(!ISUNDEF(chatid));
    if (isChatAlwaysNotifyEnabled(chatid))
    {
        LOG_warn << "setChatDnd(): always notify was enabled. Now is disabled";
        enableChatAlwaysNotify(chatid, false);
    }

    mChatDND[chatid] = timestamp;
}

void MegaPushNotificationSettingsPrivate::setGlobalChatsDnd(int64_t timestamp)
{
    assert(timestamp > 0);
    if (isGlobalChatsDndEnabled())
    {
        LOG_warn << "setChatsDnd(): global chats notifications are currently disabled."
                    " Setting a new time period for chats DND mode";
    }
    mGlobalChatsDND = timestamp;
}

void MegaPushNotificationSettingsPrivate::enableChatAlwaysNotify(MegaHandle chatid, bool enable)
{
    assert(!ISUNDEF(chatid));
    if (enable)
    {
        if (isChatDndEnabled(chatid))
        {
            LOG_warn << "enableChatAlwaysNotify(): notifications are now disabled, DND mode is enabled";
            enableChat(chatid, true);
        }

        mChatAlwaysNotify[chatid] = 1;
    }
    else
    {
        mChatAlwaysNotify.erase(chatid);
    }
}

void MegaPushNotificationSettingsPrivate::enableContacts(bool enable)
{
    mContactsDND = enable ? -1 : 0;
}

void MegaPushNotificationSettingsPrivate::enableShares(bool enable)
{
    mSharesDND = enable ? -1 : 0;
}

void MegaPushNotificationSettingsPrivate::enableChats(bool enable)
{
    mGlobalChatsDND = enable ? -1 : 0;
}

MegaCancelTokenPrivate::MegaCancelTokenPrivate()
{
}

MegaCancelTokenPrivate::MegaCancelTokenPrivate(CancelToken t)
    : cancelFlag(t)
{
}

void MegaCancelTokenPrivate::cancel()
{
    cancelFlag.cancel();
}

bool MegaCancelTokenPrivate::isCancelled() const
{
    return cancelFlag.isCancelled();
}

MegaStringList* MegaVpnClusterPrivate::getDns() const
{
    MegaStringListPrivate* dns = new MegaStringListPrivate(mCluster.getDns());
    return dns;
}

MegaStringList* MegaVpnClusterPrivate::getAdBlockingDns() const
{
    auto* adBlockingDns{new MegaStringListPrivate(mCluster.getAdBlockingDns())};
    return adBlockingDns;
}

MegaIntegerListPrivate* MegaVpnClusterMapPrivate::getKeys() const
{
    MegaIntegerListPrivate* keys = new MegaIntegerListPrivate();
    for (const auto& entry: mClusters)
    {
        keys->add(entry.first);
    }
    return keys;
}

MegaVpnClusterPrivate* MegaVpnClusterMapPrivate::get(int64_t key) const
{
    const auto it = mClusters.find(static_cast<int>(key));
    return it == mClusters.end() ? nullptr : new MegaVpnClusterPrivate(it->second);
}

MegaVpnClusterMapPrivate* MegaVpnRegionPrivate::getClusters() const
{
    return new MegaVpnClusterMapPrivate(mRegion.getClusters());
}

MegaVpnRegionListPrivate::MegaVpnRegionListPrivate(const vector<VpnRegion>& regions)
{
    for (const auto& r: regions)
    {
        mRegions.push_back(r);
    }
}

/* MegaVpnCredentialsPrivate BEGIN */
MegaVpnCredentialsPrivate::MegaVpnCredentialsPrivate(
    MapSlotIDToCredentialInfo&& mapSlotIDToCredentialInfo,
    MapClusterPublicKeys&& mapClusterPubKeys,
    std::vector<VpnRegion>&& vpnRegions):
    mMapSlotIDToCredentialInfo(std::move(mapSlotIDToCredentialInfo)),
    mMapClusterPubKeys(std::move(mapClusterPubKeys)),
    mVpnRegions(std::move(vpnRegions))
{}

MegaVpnCredentialsPrivate::MegaVpnCredentialsPrivate(const MegaVpnCredentialsPrivate& other):
    mMapSlotIDToCredentialInfo{other.mMapSlotIDToCredentialInfo},
    mMapClusterPubKeys{other.mMapClusterPubKeys},
    mVpnRegions{other.mVpnRegions}
{}

MegaIntegerList* MegaVpnCredentialsPrivate::getSlotIDs() const
{
    vector<int64_t> slotIDs;
    for (auto& it : mMapSlotIDToCredentialInfo)
    {
        slotIDs.emplace_back(it.first);
    }
    return new MegaIntegerListPrivate(std::move(slotIDs));
}

MegaStringList* MegaVpnCredentialsPrivate::getVpnRegions() const
{
    MegaStringList* regions = MegaStringList::createInstance();
    for (const auto& r : mVpnRegions)
    {
        regions->add(r.getName().c_str());
    }
    return regions;
}

MegaVpnRegionListPrivate* MegaVpnCredentialsPrivate::getVpnRegionsDetailed() const
{
    return new MegaVpnRegionListPrivate(mVpnRegions);
}

const char* MegaVpnCredentialsPrivate::getIPv4(int slotID) const
{
    auto slotInfo = mMapSlotIDToCredentialInfo.find(slotID);
    if (slotInfo != mMapSlotIDToCredentialInfo.end())
    {
        auto& credentialInfo = slotInfo->second;
        return credentialInfo.ipv4.c_str();
    }
    return nullptr;
}

const char* MegaVpnCredentialsPrivate::getIPv6(int slotID) const
{
    auto slotInfo = mMapSlotIDToCredentialInfo.find(slotID);
    if (slotInfo != mMapSlotIDToCredentialInfo.end())
    {
        auto& credentialInfo = slotInfo->second;
        return credentialInfo.ipv6.c_str();
    }
    return nullptr;
}

const char* MegaVpnCredentialsPrivate::getDeviceID(int slotID) const
{
    auto slotInfo = mMapSlotIDToCredentialInfo.find(slotID);
    if (slotInfo != mMapSlotIDToCredentialInfo.end())
    {
        auto& credentialInfo = slotInfo->second;
        return credentialInfo.deviceID.c_str();
    }
    return nullptr;
}

int MegaVpnCredentialsPrivate::getClusterID(int slotID) const
{
    auto slotInfo = mMapSlotIDToCredentialInfo.find(slotID);
    if (slotInfo != mMapSlotIDToCredentialInfo.end())
    {
        auto& credentialInfo = slotInfo->second;
        return credentialInfo.clusterID;
    }
    return -1;
}

const char* MegaVpnCredentialsPrivate::getClusterPublicKey(int clusterID) const
{
    auto pubKeyForClusterID = mMapClusterPubKeys.find(clusterID);
    if (pubKeyForClusterID != mMapClusterPubKeys.end())
    {
        return pubKeyForClusterID->second.c_str();
    }
    return nullptr;
}

MegaVpnCredentials* MegaVpnCredentialsPrivate::copy() const
{
    return new MegaVpnCredentialsPrivate(*this);
}
/* MegaVpnCredentials END */

MegaNodeTreePrivate::MegaNodeTreePrivate(const MegaNodeTree* nodeTreeChild,
                                         const std::string& name,
                                         const std::string& s4AttributeValue,
                                         const MegaCompleteUploadData* completeUploadData,
                                         MegaHandle sourceHandle,
                                         MegaHandle nodeHandle):
    mName{name},
    mS4AttributeValue{s4AttributeValue},
    mSourceHandle{sourceHandle},
    mNodeHandle{nodeHandle}
{
    if (nodeTreeChild)
    {
        mNodeTreeChild.reset(nodeTreeChild->copy());
    }
    if (completeUploadData)
    {
        mCompleteUploadData.reset(completeUploadData->copy());
    }
}

MegaNetworkConnectivityTestResultsPrivate* MegaNetworkConnectivityTestResultsPrivate::copy() const
{
    return new MegaNetworkConnectivityTestResultsPrivate(mIPv4, mIPv4DNS, mIPv6, mIPv6DNS);
}

MegaNodeTree* MegaNodeTreePrivate::getNodeTreeChild() const
{
    return mNodeTreeChild.get();
}

const std::string& MegaNodeTreePrivate::getName() const
{
    return mName;
}

const std::string& MegaNodeTreePrivate::getS4AttributeValue() const
{
    return mS4AttributeValue;
}

const MegaCompleteUploadData* MegaNodeTreePrivate::getCompleteUploadData() const
{
    return mCompleteUploadData.get();
}

MegaHandle MegaNodeTreePrivate::getNodeHandle() const
{
    return mNodeHandle;
}

void MegaNodeTreePrivate::setNodeHandle(const MegaHandle& nodeHandle)
{
    mNodeHandle = nodeHandle;
}

MegaNodeTree* MegaNodeTreePrivate::copy() const
{
    MegaNodeTreePrivate* megaNodeTreeCopy = new MegaNodeTreePrivate(
        mNodeTreeChild.get(),
        mName,
        mS4AttributeValue,
        mCompleteUploadData.get(),
        mSourceHandle,
        mNodeHandle
    );

    return megaNodeTreeCopy;
}

MegaCompleteUploadDataPrivate::MegaCompleteUploadDataPrivate(const std::string& fingerprint,
                                                             const std::string& string64UploadToken,
                                                             const std::string& string64FileKey):
    mFingerprint{fingerprint},
    mString64UploadToken{string64UploadToken},
    mString64FileKey{string64FileKey}
{}

const std::string& MegaCompleteUploadDataPrivate::getFingerprint() const
{
    return mFingerprint;
}

const std::string& MegaCompleteUploadDataPrivate::getString64UploadToken() const
{
    return mString64UploadToken;
}

const std::string& MegaCompleteUploadDataPrivate::getString64FileKey() const
{
    return mString64FileKey;
}

MegaCompleteUploadData* MegaCompleteUploadDataPrivate::copy() const
{
    return new MegaCompleteUploadDataPrivate(*this);
}

MegaFuseExecutorFlagsPrivate::MegaFuseExecutorFlagsPrivate(common::TaskExecutorFlags& flags)
  : MegaFuseExecutorFlags()
  , mFlags(flags)
{
}

MegaStringList* MegaNotificationPrivate::getRenderModes() const
{
    if (mNotification.renderModes.empty())
    {
        return nullptr;
    }

    MegaStringList* modes = MegaStringList::createInstance();
    for (const auto& m: mNotification.renderModes)
    {
        modes->add(m.first.c_str());
    }

    return modes;
}

MegaStringMap* MegaNotificationPrivate::getRenderModeFields(const char* mode) const
{
    if (!mode || mNotification.renderModes.empty())
    {
        return nullptr;
    }

    for (const auto& m: mNotification.renderModes)
    {
        if (m.first == mode)
        {
            return new MegaStringMapPrivate(&m.second);
        }
    }

    return nullptr;
}

size_t MegaFuseExecutorFlagsPrivate::getMinThreadCount() const
{
    return mFlags.mMinWorkers;
}

size_t MegaFuseExecutorFlagsPrivate::getMaxThreadCount() const
{
    return mFlags.mMaxWorkers;
}

size_t MegaFuseExecutorFlagsPrivate::getMaxThreadIdleTime() const
{
    return static_cast<size_t>(mFlags.mIdleTime.count());
}

bool MegaFuseExecutorFlagsPrivate::setMaxThreadCount(size_t max)
{
    if (!max)
        return false;

    mFlags.mMaxWorkers = max;

    return true;
}

void MegaFuseExecutorFlagsPrivate::setMinThreadCount(size_t min)
{
    mFlags.mMinWorkers = min;
}

void MegaFuseExecutorFlagsPrivate::setMaxThreadIdleTime(size_t max)
{
    mFlags.mIdleTime = std::chrono::seconds(max);
}

MegaFuseInodeCacheFlagsPrivate::MegaFuseInodeCacheFlagsPrivate(fuse::InodeCacheFlags& flags)
  : MegaFuseInodeCacheFlags()
  , mFlags(flags)
{
}

size_t MegaFuseInodeCacheFlagsPrivate::getCleanAgeThreshold() const
{
    return static_cast<size_t>(mFlags.mCleanAgeThreshold.count());
}

size_t MegaFuseInodeCacheFlagsPrivate::getCleanInterval() const
{
    return static_cast<size_t>(mFlags.mCleanInterval.count());
}

size_t MegaFuseInodeCacheFlagsPrivate::getCleanSizeThreshold() const
{
    return mFlags.mCleanSizeThreshold;
}

size_t MegaFuseInodeCacheFlagsPrivate::getMaxSize() const
{
    return mFlags.mMaxSize;
}

void MegaFuseInodeCacheFlagsPrivate::setCleanAgeThreshold(std::size_t seconds)
{
    mFlags.mCleanAgeThreshold = std::chrono::seconds(seconds);
}

void MegaFuseInodeCacheFlagsPrivate::setCleanInterval(std::size_t seconds)
{
    mFlags.mCleanInterval = std::chrono::seconds(seconds);
}

void MegaFuseInodeCacheFlagsPrivate::setCleanSizeThreshold(std::size_t size)
{
    mFlags.mCleanSizeThreshold = size;
}

void MegaFuseInodeCacheFlagsPrivate::setMaxSize(std::size_t size)
{
    mFlags.mMaxSize = size;
}

MegaFuseFlagsPrivate::MegaFuseFlagsPrivate(const fuse::ServiceFlags& flags)
  : MegaFuseFlags()
  , mFlags(flags)
  , mInodeCacheFlags(mFlags.mInodeCacheFlags)
  , mMountExecutorFlags(mFlags.mMountExecutorFlags)
  , mSubsystemExecutorFlags(mFlags.mServiceExecutorFlags)
{
}

MegaFuseFlags* MegaFuseFlagsPrivate::copy() const
{
    return new MegaFuseFlagsPrivate(mFlags);
}

const fuse::ServiceFlags& MegaFuseFlagsPrivate::getFlags() const
{
    return mFlags;
}

size_t MegaFuseFlagsPrivate::getFlushDelay() const
{
    return static_cast<size_t>(mFlags.mFlushDelay.count());
}

int MegaFuseFlagsPrivate::getLogLevel() const
{
    return static_cast<int>(mFlags.mLogLevel);
}

int MegaFuseFlagsPrivate::getFileExplorerView() const
{
    return static_cast<int>(mFlags.mFileExplorerView);
}

MegaFuseInodeCacheFlags* MegaFuseFlagsPrivate::getInodeCacheFlags()
{
    return &mInodeCacheFlags;
}

MegaFuseExecutorFlags* MegaFuseFlagsPrivate::getMountExecutorFlags()
{
    return &mMountExecutorFlags;
}

MegaFuseExecutorFlags* MegaFuseFlagsPrivate::getSubsystemExecutorFlags()
{
    return &mSubsystemExecutorFlags;
}

void MegaFuseFlagsPrivate::setFlushDelay(size_t seconds)
{
    mFlags.mFlushDelay = std::chrono::seconds(seconds);
}

void MegaFuseFlagsPrivate::setLogLevel(int level)
{
    mFlags.mLogLevel = static_cast<mega::LogLevel>(level);
}

void MegaFuseFlagsPrivate::setFileExplorerView(int view)
{
    mFlags.mFileExplorerView = static_cast<fuse::FileExplorerView>(view);
}

MegaMountPrivate::MegaMountPrivate()
  : MegaMount()
  , mFlags(std::make_unique<MegaMountFlagsPrivate>())
  , mHandle(UNDEF)
  , mPath()
{
}

MegaMountPrivate::MegaMountPrivate(const fuse::MountInfo& info)
  : MegaMount()
  , mFlags(std::make_unique<MegaMountFlagsPrivate>(info.mFlags))
  , mHandle(info.mHandle.as8byte())
  , mPath(info.mPath.toPath(false))
{
}

MegaMountPrivate::MegaMountPrivate(const MegaMountPrivate& other)
  : MegaMount(other)
  , mFlags(other.mFlags->copy())
  , mHandle(other.mHandle)
  , mPath(other.mPath)
{
}

fuse::MountInfo MegaMountPrivate::asInfo() const
{
    fuse::MountInfo info;

    info.mFlags = static_cast<MegaMountFlagsPrivate&>(*mFlags).getFlags();
    info.mHandle.set6byte(mHandle);
    if (!mPath.empty())
    {
        info.mPath = LocalPath::fromAbsolutePath(mPath);
    }

    return info;
}

MegaMount* MegaMountPrivate::copy() const
{
    return new MegaMountPrivate(*this);
}

MegaMountFlags* MegaMountPrivate::getFlags() const
{
    return mFlags.get();
}

MegaHandle MegaMountPrivate::getHandle() const
{
    return mHandle;
}

const char* MegaMountPrivate::getPath() const
{
    return mPath.c_str();
}

void MegaMountPrivate::setFlags(const MegaMountFlags* flags)
{
    assert(flags);

    mFlags.reset(flags->copy());
}

void MegaMountPrivate::setHandle(MegaHandle handle)
{
    mHandle = handle;
}

void MegaMountPrivate::setPath(const char* path)
{
    assert(path);

    mPath = path;
}

MegaMountFlagsPrivate::MegaMountFlagsPrivate(const fuse::MountFlags& flags)
  : MegaMountFlags()
  , mFlags(flags)
{
}

MegaMountFlags* MegaMountFlagsPrivate::copy() const
{
    return new MegaMountFlagsPrivate(*this);
}

bool MegaMountFlagsPrivate::getEnableAtStartup() const
{
    return mFlags.mEnableAtStartup;
}

const fuse::MountFlags& MegaMountFlagsPrivate::getFlags() const
{
    return mFlags;
}

const char* MegaMountFlagsPrivate::getName() const
{
    return mFlags.mName.c_str();
}

bool MegaMountFlagsPrivate::getPersistent() const
{
    return mFlags.mPersistent;
}

bool MegaMountFlagsPrivate::getReadOnly() const
{
    return mFlags.mReadOnly;
}

void MegaMountFlagsPrivate::setEnableAtStartup(bool enable)
{
    mFlags.mEnableAtStartup = enable;
    mFlags.mPersistent |= true;
}

void MegaMountFlagsPrivate::setName(const char* name)
{
    assert(name);

    mFlags.mName = name;
}

void MegaMountFlagsPrivate::setPersistent(bool persistent)
{
    mFlags.mEnableAtStartup &= persistent;
    mFlags.mPersistent = persistent;
}

void MegaMountFlagsPrivate::setReadOnly(bool readOnly)
{
    mFlags.mReadOnly = readOnly;
}

MegaMountListPrivate::MegaMountListPrivate(fuse::MountInfoVector&& mounts)
  : MegaMountList()
  , mMounts()
{
    for (auto& m : mounts)
    {
        auto mount = std::make_unique<MegaMountPrivate>(std::move(m));

        mMounts.emplace_back(std::move(mount));
    }
}

MegaMountListPrivate::MegaMountListPrivate(const MegaMountListPrivate& other)
  : MegaMountList()
  , mMounts()
{
    mMounts.reserve(other.mMounts.size());

    for (const auto& m : other.mMounts)
        mMounts.emplace_back(m->copy());
}

MegaMountList* MegaMountListPrivate::copy() const
{
    return new MegaMountListPrivate(*this);
}

const MegaMount* MegaMountListPrivate::get(size_t index) const
{
    if (index < mMounts.size())
        return mMounts[index].get();

    return nullptr;
}

size_t MegaMountListPrivate::size() const
{
    return mMounts.size();
}

MegaCancelSubscriptionReasonPrivate::MegaCancelSubscriptionReasonPrivate(const char* text,
                                                                         const char* position):
    mText{text ? text : ""},
    mPosition{position ? position : ""}
{}

const char* MegaCancelSubscriptionReasonPrivate::text() const
{
    return mText.c_str();
}

const char* MegaCancelSubscriptionReasonPrivate::position() const
{
    return mPosition.c_str();
}

MegaCancelSubscriptionReasonPrivate* MegaCancelSubscriptionReasonPrivate::copy() const
{
    return new MegaCancelSubscriptionReasonPrivate(*this);
}

void MegaCancelSubscriptionReasonListPrivate::add(const MegaCancelSubscriptionReason* reason)
{
    mReasons.emplace_back(reason->copy());
}

const MegaCancelSubscriptionReason* MegaCancelSubscriptionReasonListPrivate::get(size_t index) const
{
    return index >= mReasons.size() ? nullptr : mReasons[index].get();
}

size_t MegaCancelSubscriptionReasonListPrivate::size() const
{
    return mReasons.size();
}

MegaCancelSubscriptionReasonListPrivate* MegaCancelSubscriptionReasonListPrivate::copy() const
{
    return new MegaCancelSubscriptionReasonListPrivate(*this);
}

MegaDiscountCodePrivate::MegaDiscountCodePrivate(DiscountCode&& discountCode):
    mDiscountCode{std::move(discountCode)}
{}

MegaDiscountCode* MegaDiscountCodePrivate::copy() const
{
    return new MegaDiscountCodePrivate(*this);
}

const char* MegaDiscountCodePrivate::getCode() const
{
    return mDiscountCode.alfanumDiscountCode.c_str();
}

int MegaDiscountCodePrivate::getItem() const
{
    return mDiscountCode.item;
}

int MegaDiscountCodePrivate::getAccountLevel() const
{
    return mDiscountCode.accountLevel;
}

int MegaDiscountCodePrivate::getMonths() const
{
    return mDiscountCode.numMonths;
}

int MegaDiscountCodePrivate::getPercentageDiscount() const
{
    return mDiscountCode.percentageDiscount;
}

int MegaDiscountCodePrivate::getBehaviorType() const
{
    return mDiscountCode.behaviourType;
}

MegaDiscountCodeListPrivate::MegaDiscountCodeListPrivate():
    mDiscountCodes{}
{}

MegaDiscountCodeList* MegaDiscountCodeListPrivate::copy() const
{
    return new MegaDiscountCodeListPrivate(*this);
}

int MegaDiscountCodeListPrivate::size() const
{
    return static_cast<int>(mDiscountCodes.size());
}

const MegaDiscountCode* MegaDiscountCodeListPrivate::get(int i) const
{
    if (i < 0 || i >= static_cast<int>(mDiscountCodes.size()))
    {
        return nullptr;
    }
    return &mDiscountCodes[static_cast<size_t>(i)];
}

void MegaDiscountCodeListPrivate::add(MegaDiscountCodePrivate&& discountCode)
{
    mDiscountCodes.emplace_back(std::move(discountCode));
}

MegaDiscountCodeInfoPrivate::MegaDiscountCodeInfoPrivate(
    DiscountCodeInfoExtended&& discountCodeInfo):
    mDiscountCodeInfo{std::move(discountCodeInfo)}
{}

MegaDiscountCodeInfo* MegaDiscountCodeInfoPrivate::copy() const
{
    return new MegaDiscountCodeInfoPrivate(*this);
}

const char* MegaDiscountCodeInfoPrivate::getCode() const
{
    return mDiscountCodeInfo.alfanumDiscountCode.c_str();
}

int MegaDiscountCodeInfoPrivate::getItem() const
{
    return mDiscountCodeInfo.item;
}

int MegaDiscountCodeInfoPrivate::getAccountLevel() const
{
    return mDiscountCodeInfo.accountLevel;
}

int MegaDiscountCodeInfoPrivate::getMonths() const
{
    return mDiscountCodeInfo.numMonths;
}

int MegaDiscountCodeInfoPrivate::getPercentageDiscount() const
{
    return mDiscountCodeInfo.percentageDiscount;
}

int MegaDiscountCodeInfoPrivate::getBehaviorType() const
{
    return mDiscountCodeInfo.behaviourType;
}

int MegaDiscountCodeInfoPrivate::getExpiry() const
{
    return mDiscountCodeInfo.expiry;
}

int MegaDiscountCodeInfoPrivate::getCompulsorySubscription() const
{
    return mDiscountCodeInfo.compulsorySubscription;
}

int MegaDiscountCodeInfoPrivate::getMultiDiscount() const
{
    return mDiscountCodeInfo.multiDiscount;
}

int MegaDiscountCodeInfoPrivate::getTaxValue() const
{
    return mDiscountCodeInfo.txva;
}

MegaStringIntegerMap* MegaDiscountCodeInfoPrivate::getFeatures() const
{
    MegaStringIntegerMapPrivate* featuresMap = new MegaStringIntegerMapPrivate();
    std::for_each(mDiscountCodeInfo.features.begin(),
                  mDiscountCodeInfo.features.end(),
                  [featuresMap](const std::pair<const std::string, int>& f)
                  {
                      featuresMap->set(f.first.c_str(), f.second);
                  });

    return featuresMap;
}

bool MegaDiscountCodeInfoPrivate::isTaxExempt() const
{
    return (mDiscountCodeInfo.taxExempt & DiscountCodeInfoExtended::TaxFlags::TAX_EXEMPT) != 0;
}

bool MegaDiscountCodeInfoPrivate::isTaxAppliedOnTop() const
{
    return (mDiscountCodeInfo.taxExempt & DiscountCodeInfoExtended::TaxFlags::TAX_ON_TOP) != 0;
}

int MegaDiscountCodeInfoPrivate::getTaxRate() const
{
    return mDiscountCodeInfo.taxRate;
}

const char* MegaDiscountCodeInfoPrivate::getTaxName() const
{
    return mDiscountCodeInfo.taxName.c_str();
}

const char* MegaDiscountCodeInfoPrivate::getTaxCountry() const
{
    return mDiscountCodeInfo.taxCountry.c_str();
}

double MegaDiscountCodeInfoPrivate::getEuroTotalPrice() const
{
    return mDiscountCodeInfo.euroTotalPrice;
}

double MegaDiscountCodeInfoPrivate::getEuroDiscountAmount() const
{
    return mDiscountCodeInfo.euroDiscountAmount;
}

double MegaDiscountCodeInfoPrivate::getEuroDiscountedTotalPrice() const
{
    return mDiscountCodeInfo.euroDiscountedTotalPrice;
}

double MegaDiscountCodeInfoPrivate::getEuroDiscountedMonthlyPrice() const
{
    return mDiscountCodeInfo.euroDiscountedMonthlyPrice;
}

double MegaDiscountCodeInfoPrivate::getEuroTotalPriceNet() const
{
    return mDiscountCodeInfo.euroTotalPriceNet;
}

double MegaDiscountCodeInfoPrivate::getEuroDiscountAmountNet() const
{
    return mDiscountCodeInfo.euroDiscountAmountNet;
}

double MegaDiscountCodeInfoPrivate::getEuroDiscountedTotalPriceNet() const
{
    return mDiscountCodeInfo.euroDiscountedTotalPriceNet;
}

double MegaDiscountCodeInfoPrivate::getEuroDiscountedMonthlyPriceNet() const
{
    return mDiscountCodeInfo.euroDiscountedMonthlyPriceNet;
}

const char* MegaDiscountCodeInfoPrivate::getLocalCurrencyCode() const
{
    return mDiscountCodeInfo.localCurrencyCode.c_str();
}

const char* MegaDiscountCodeInfoPrivate::getLocalCurrencySymbol() const
{
    return mDiscountCodeInfo.localCurrencySymbol.c_str();
}

double MegaDiscountCodeInfoPrivate::getLocalTotalPrice() const
{
    return mDiscountCodeInfo.localTotalPrice;
}

double MegaDiscountCodeInfoPrivate::getLocalDiscountAmount() const
{
    return mDiscountCodeInfo.localDiscountAmount;
}

double MegaDiscountCodeInfoPrivate::getLocalDiscountedTotalPrice() const
{
    return mDiscountCodeInfo.localDiscountedTotalPrice;
}

double MegaDiscountCodeInfoPrivate::getLocalDiscountedMonthlyPrice() const
{
    return mDiscountCodeInfo.localDiscountedMonthlyPrice;
}

double MegaDiscountCodeInfoPrivate::getLocalTotalPriceNet() const
{
    return mDiscountCodeInfo.localTotalPriceNet;
}

double MegaDiscountCodeInfoPrivate::getLocalDiscountAmountNet() const
{
    return mDiscountCodeInfo.localDiscountAmountNet;
}

double MegaDiscountCodeInfoPrivate::getLocalDiscountedTotalPriceNet() const
{
    return mDiscountCodeInfo.localDiscountedTotalPriceNet;
}

double MegaDiscountCodeInfoPrivate::getLocalDiscountedMonthlyPriceNet() const
{
    return mDiscountCodeInfo.localDiscountedMonthlyPriceNet;
}

MegaFileServiceReclaimOptionsPrivate::MegaFileServiceReclaimOptionsPrivate() = default;

MegaFileServiceReclaimOptionsPrivate::MegaFileServiceReclaimOptionsPrivate(
    const file_service::ReclaimOptions& options):
    mReclaim(options)
{}

MegaFileServiceReclaimOptionsPrivate::MegaFileServiceReclaimOptionsPrivate(
    const MegaFileServiceReclaimOptionsPrivate&) = default;

MegaFileServiceReclaimOptionsPrivate::~MegaFileServiceReclaimOptionsPrivate() = default;

MegaFileServiceReclaimOptions* MegaFileServiceReclaimOptionsPrivate::copy() const
{
    return new MegaFileServiceReclaimOptionsPrivate(*this);
}

int MegaFileServiceReclaimOptionsPrivate::getAgeThreshold() const
{
    return static_cast<int>(mReclaim.mAgeThreshold.count());
}

void MegaFileServiceReclaimOptionsPrivate::setAgeThreshold(int ageThreshold)
{
    mReclaim.mAgeThreshold = std::chrono::minutes(std::max(0, ageThreshold));
}

std::size_t MegaFileServiceReclaimOptionsPrivate::getBatchSize() const
{
    return mReclaim.mBatchSize;
}

void MegaFileServiceReclaimOptionsPrivate::setBatchSize(std::size_t batchSize)
{
    mReclaim.mBatchSize = batchSize;
}

uint64_t MegaFileServiceReclaimOptionsPrivate::getDelay() const
{
    return static_cast<uint64_t>(mReclaim.mDelay.count());
}

void MegaFileServiceReclaimOptionsPrivate::setDelay(uint64_t seconds)
{
    mReclaim.mDelay = std::chrono::seconds(seconds);
}

uint64_t MegaFileServiceReclaimOptionsPrivate::getPeriod() const
{
    return static_cast<uint64_t>(mReclaim.mPeriod.count());
}

void MegaFileServiceReclaimOptionsPrivate::setPeriod(uint64_t seconds)
{
    mReclaim.mPeriod = std::chrono::seconds(seconds);
}

uint64_t MegaFileServiceReclaimOptionsPrivate::getReclaimTarget() const
{
    return mReclaim.mReclaimTarget;
}

void MegaFileServiceReclaimOptionsPrivate::setReclaimTarget(uint64_t bytes)
{
    mReclaim.mReclaimTarget = bytes;
}

int64_t MegaFileServiceReclaimOptionsPrivate::getReclaimThreshold() const
{
    const auto& reclaimThreshold = mReclaim.mReclaimThreshold;
    if (reclaimThreshold)
        return static_cast<int64_t>(*reclaimThreshold);
    else
        return -1;
}

void MegaFileServiceReclaimOptionsPrivate::setReclaimThreshold(int64_t bytes)
{
    if (bytes >= 0)
        mReclaim.mReclaimThreshold = static_cast<uint64_t>(bytes);
    else
        mReclaim.mReclaimThreshold = std::nullopt;
}

file_service::ReclaimOptions MegaFileServiceReclaimOptionsPrivate::getOptions() const
{
    return mReclaim;
}

MegaFileServiceStorageInfoPrivate::MegaFileServiceStorageInfoPrivate(
    const file_service::StorageInfo& storageInfo):
    mStorageInfo(storageInfo)
{}

uint64_t MegaFileServiceStorageInfoPrivate::getReclaimableSize() const
{
    return mStorageInfo.mReclaimableSize;
}

uint64_t MegaFileServiceStorageInfoPrivate::getAllocatedSize() const
{
    return mStorageInfo.mAllocatedSize;
}

std::unique_ptr<FileSystemAccess> createFSA()
{
    auto fsaccess = std::unique_ptr<FileSystemAccess>{new FSACCESS_CLASS};
#ifdef __ANDROID__
    if (!AndroidFileSystemAccess::isFileWrapperActive(fsaccess.get()))
    {
        LOG_verbose << "[mega::createFSA] JNI FileWrapper not present. Creating "
                       "LinuxFileSystemAccess instead of AndroidFileSystemAccess";
        fsaccess.reset(new LinuxFileSystemAccess);
    }
#endif
    return fsaccess;
}

void MegaApiImpl::notify_network_activity(int networkActivityChannel,
                                          int networkActivityType,
                                          int code)
{
    MegaEventPrivate* event = new MegaEventPrivate(MegaEvent::EVENT_NETWORK_ACTIVITY);
    event->setNumber("channel", networkActivityChannel);
    event->setNumber("activity_type", networkActivityType);
    event->setNumber("error_code", code);
    fireOnEvent(event);
}

void MegaApiImpl::notify_transfers_resumed(const std::vector<uint32_t>& uniqueIds)
{
    MegaEventPrivate* event = new MegaEventPrivate(MegaEvent::EVENT_TRANSFERS_RESUMED);
    event->setIntegerList(new MegaIntegerListPrivate(uniqueIds));
    fireOnEvent(event);
}

} // namespace mega
