/****************************************************************************
 *    Copyright (C) 2017-2023 Savoir-faire Linux Inc.                       *
 *   Author: Nicolas Jäger <nicolas.jager@savoirfairelinux.com>             *
 *   Author: Sébastien Blin <sebastien.blin@savoirfairelinux.com>           *
 *   Author: Kateryna Kostiuk <kateryna.kostiuk@savoirfairelinux.com>       *
 *   Author: Andreas Traczyk <andreas.traczyk@savoirfairelinux.com>         *
 *                                                                          *
 *   This library is free software; you can redistribute it and/or          *
 *   modify it under the terms of the GNU Lesser General Public             *
 *   License as published by the Free Software Foundation; either           *
 *   version 2.1 of the License, or (at your option) any later version.     *
 *                                                                          *
 *   This library 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.  See the GNU      *
 *   Lesser General Public License for more details.                        *
 *                                                                          *
 *   You should have received a copy of the GNU General Public License      *
 *   along with this program.  If not, see <http://www.gnu.org/licenses/>.  *
 ***************************************************************************/
#include "storagehelper.h"

#include "api/profile.h"
#include "api/conversation.h"
#include "api/datatransfer.h"
#include "api/lrc.h"
#include "uri.h"
#include "vcard.h"

#include <account_const.h>
#include <datatransfer_interface.h>

#include <QImage>
#include <QByteArray>
#include <QBuffer>
#include <QLockFile>
#include <QJsonObject>
#include <QJsonDocument>

#include <fstream>
#if !defined(Q_OS_LINUX) || __GNUC__ > 8
#include <filesystem>
#endif
#include <thread>
#include <cstring>

namespace lrc {

namespace authority {

namespace storage {

QString
getPath()
{
#ifdef Q_OS_WIN
    auto definedDataDir = qEnvironmentVariable("JAMI_DATA_HOME");
    if (!definedDataDir.isEmpty())
        return QDir(definedDataDir).absolutePath() + "/";
#endif
    QDir dataDir(QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation));
    // Avoid to depends on the client name.
    dataDir.cdUp();
    return dataDir.absolutePath() + "/jami/";
}

static QString
profileVcardPath(const QString& accountId, const QString& uri)
{
    auto accountLocalPath = getPath() + accountId + QDir::separator();
    if (uri.isEmpty())
        return accountLocalPath + "profile.vcf";

    auto fileName = QString(uri.toUtf8().toBase64());
    return accountLocalPath + "profiles" + QDir::separator() + fileName + ".vcf";
}

static QString
stringFromJSON(const QJsonObject& json)
{
    QJsonDocument doc(json);
    return QString::fromLocal8Bit(doc.toJson(QJsonDocument::Compact));
}

static QJsonObject
JSONFromString(const QString& str)
{
    QJsonObject json;
    QJsonDocument doc = QJsonDocument::fromJson(str.toUtf8());

    if (!doc.isNull()) {
        if (doc.isObject()) {
            json = doc.object();
        } else {
            qDebug() << "Document is not a JSON object: " << str;
        }
    } else {
        qDebug() << "Invalid JSON: " << str;
    }
    return json;
}

static QString
JSONStringFromInitList(const std::initializer_list<QPair<QString, QJsonValue>> args)
{
    QJsonObject jsonObject(args);
    return stringFromJSON(jsonObject);
}

static QString
readJSONValue(const QJsonObject& json, const QString& key)
{
    if (!json.isEmpty() && json.contains(key) && json[key].isString()) {
        if (json[key].isString()) {
            return json[key].toString();
        }
    }
    return {};
}

static void
writeJSONValue(QJsonObject& json, const QString& key, const QString& value)
{
    json[key] = value;
}

QString
prepareUri(const QString& uri, api::profile::Type type)
{
    URI uriObject(uri);
    switch (type) {
    case api::profile::Type::SIP:
        return uriObject.format(URI::Section::USER_INFO | URI::Section::HOSTNAME);
        break;
    case api::profile::Type::JAMI:
        return uriObject.format(URI::Section::USER_INFO);
        break;
    case api::profile::Type::INVALID:
    case api::profile::Type::PENDING:
    case api::profile::Type::TEMPORARY:
    case api::profile::Type::COUNT__:
    default:
        return uri;
    }
}

QString
getFormattedCallDuration(const std::time_t duration)
{
    if (duration == 0)
        return {};
    std::string formattedString;
    auto minutes = duration / 60;
    auto seconds = duration % 60;
    if (minutes > 0) {
        formattedString += std::to_string(minutes) + ":";
        if (formattedString.length() == 2) {
            formattedString = "0" + formattedString;
        }
    } else {
        formattedString += "00:";
    }
    if (seconds < 10)
        formattedString += "0";
    formattedString += std::to_string(seconds);
    return QString::fromStdString(formattedString);
}

QString
getCallInteractionStringNonSwarm(bool isSelf, const std::time_t& duration)
{
    if (duration < 0) {
        if (isSelf) {
            return QObject::tr("Outgoing call");
        } else {
            return QObject::tr("Incoming call");
        }
    } else if (isSelf) {
        if (duration) {
            return QObject::tr("Outgoing call") + " - " + getFormattedCallDuration(duration);
        } else {
            return QObject::tr("Missed outgoing call");
        }
    } else {
        if (duration) {
            return QObject::tr("Incoming call") + " - " + getFormattedCallDuration(duration);
        } else {
            return QObject::tr("Missed incoming call");
        }
    }
}

QString
getCallInteractionString(bool isSelf, const api::interaction::Info& info)
{
    if (!info.confId.isEmpty()) {
        if (info.duration <= 0) {
            return QObject::tr("Join call");
        }
    }
    return getCallInteractionStringNonSwarm(isSelf, info.duration);
}

QString
getContactInteractionString(const QString& authorUri, const api::interaction::Status& status)
{
    if (authorUri.isEmpty()) {
        return QObject::tr("Contact added");
    } else {
        if (status == api::interaction::Status::UNKNOWN) {
            return QObject::tr("Invitation received");
        } else if (status == api::interaction::Status::SUCCESS) {
            return QObject::tr("Invitation accepted");
        }
    }
    return {};
}

namespace vcard {
QString
compressedAvatar(const QString& image)
{
    QImage qimage;
    // Avoid to use all formats. Some seems bugguy, like libpbf, asking
    // for a QGuiApplication for QFontDatabase
    auto ret = qimage.loadFromData(QByteArray::fromBase64(image.toUtf8()), "JPEG");
    if (!ret)
        ret = qimage.loadFromData(QByteArray::fromBase64(image.toUtf8()), "PNG");
    if (!ret) {
        qDebug() << "vCard image loading failed";
        return "";
    }
    QByteArray bArray;
    QBuffer buffer(&bArray);
    buffer.open(QIODevice::WriteOnly);

    auto size = qMin(qimage.width(), qimage.height());
    auto rect = QRect((qimage.width() - size) / 2, (qimage.height() - size) / 2, size, size);
    constexpr auto quality = 88; // Same as android, between 80 and 90 jpeg compression changes a lot
    constexpr auto maxSize = 16000 * 8; // Because 16*3 (rgb) = 48k, which is a valid size for the
                                        // DHT and * 8 because typical jpeg compression
    // divides the size per 8
    while (size * size > maxSize)
        size /= 2;
    qimage.copy(rect).scaled({size, size}, Qt::KeepAspectRatio).save(&buffer, "JPEG", quality);
    auto b64Img = bArray.toBase64().trimmed();
    return QString::fromLocal8Bit(b64Img.constData(), b64Img.length());
}

QString
profileToVcard(const api::profile::Info& profileInfo, bool compressImage)
{
    using namespace api;
    bool compressedImage = std::strncmp(profileInfo.avatar.toStdString().c_str(), "/9j/", 4) == 0;
    if (compressedImage && !compressImage) {
        compressImage = false;
    }
    QString vCardStr = vCard::Delimiter::BEGIN_TOKEN;
    vCardStr += vCard::Delimiter::END_LINE_TOKEN;
    vCardStr += vCard::Property::VERSION;
    vCardStr += ":2.1";
    vCardStr += vCard::Delimiter::END_LINE_TOKEN;
    vCardStr += vCard::Property::FORMATTED_NAME;
    vCardStr += ":";
    vCardStr += profileInfo.alias;
    vCardStr += vCard::Delimiter::END_LINE_TOKEN;
    if (profileInfo.type == profile::Type::JAMI) {
        vCardStr += vCard::Property::TELEPHONE;
        vCardStr += vCard::Delimiter::SEPARATOR_TOKEN;
        vCardStr += "other:ring:";
        vCardStr += profileInfo.uri;
        vCardStr += vCard::Delimiter::END_LINE_TOKEN;
    } else {
        vCardStr += vCard::Property::TELEPHONE;
        vCardStr += ":";
        vCardStr += profileInfo.uri;
        vCardStr += vCard::Delimiter::END_LINE_TOKEN;
    }
    vCardStr += vCard::Property::PHOTO;
    vCardStr += vCard::Delimiter::SEPARATOR_TOKEN;
    vCardStr += vCard::Property::BASE64;
    vCardStr += vCard::Delimiter::SEPARATOR_TOKEN;
    if (compressImage) {
        vCardStr += vCard::Property::TYPE_JPEG;
        vCardStr += ":";
        vCardStr += compressedImage ? profileInfo.avatar : compressedAvatar(profileInfo.avatar);
    } else {
        vCardStr += compressedImage ? vCard::Property::TYPE_JPEG : vCard::Property::TYPE_PNG;
        vCardStr += ":";
        vCardStr += profileInfo.avatar;
    }
    vCardStr += vCard::Delimiter::END_LINE_TOKEN;
    vCardStr += vCard::Delimiter::END_TOKEN;
    return vCardStr;
}

void
setProfile(const QString& accountId, const api::profile::Info& profileInfo, const bool isPeer)
{
    auto vcard = vcard::profileToVcard(profileInfo);
    auto path = profileVcardPath(accountId, isPeer ? profileInfo.uri : "");
    QLockFile lf(path + ".lock");
    QFile file(path);
    QFileInfo fileInfo(path);
    auto dir = fileInfo.dir();
    if (!dir.exists()) {
#if !defined(Q_OS_LINUX) || __GNUC__ > 8
        if (!std::filesystem::create_directory(dir.path().toStdString())) {
#endif
            qWarning() << "Cannot create " << dir.path();
#if !defined(Q_OS_LINUX) || __GNUC__ > 8
        }
#endif
    }
    if (!lf.lock()) {
        qWarning().noquote() << "Can't lock file for writing: " << file.fileName();
        return;
    }
    if (!file.open(QIODevice::WriteOnly)) {
        lf.unlock();
        qWarning().noquote() << "Can't open file for writing: " << file.fileName();
        return;
    }
    QTextStream(&file) << vcard;
    file.close();
    lf.unlock();
}
} // namespace vcard

VectorString
getConversationsWithPeer(Database& db, const QString& participant_uri)
{
    return db
        .select("id",
                "conversations",
                "participant=:participant",
                {{":participant", participant_uri}})
        .payloads;
}

VectorString
getPeerParticipantsForConversation(Database& db, const QString& conversationId)
{
    return db.select("participant", "conversations", "id=:id", {{":id", conversationId}}).payloads;
}

void
createOrUpdateProfile(const QString& accountId,
                      const api::profile::Info& profileInfo,
                      const bool isPeer)
{
    if (isPeer) {
        auto contact = storage::buildContactFromProfile(accountId,
                                                        profileInfo.uri,
                                                        profileInfo.type);
        if (!profileInfo.alias.isEmpty())
            contact.profileInfo.alias = profileInfo.alias;
        if (!profileInfo.avatar.isEmpty())
            contact.profileInfo.avatar = profileInfo.avatar;
        vcard::setProfile(accountId, contact.profileInfo, isPeer);
        return;
    }
    vcard::setProfile(accountId, profileInfo, isPeer);
}

void
removeProfile(const QString& accountId, const QString& peerUri)
{
    auto path = profileVcardPath(accountId, peerUri);
    if (!QFile::remove(path)) {
        qWarning() << "Couldn't remove vcard for" << peerUri << "at" << path;
    }
}

QString
getAccountAvatar(const QString& accountId)
{
    auto accountLocalPath = getPath() + accountId + "/";
    QString filePath;
    filePath = accountLocalPath + "profile.vcf";
    QFile file(filePath);
    if (!file.open(QIODevice::ReadOnly))
        return {};
    const auto vCard = lrc::vCard::utils::toHashMap(file.readAll());
    const auto photo = (vCard.find(vCard::Property::PHOTO_PNG) == vCard.end())
                           ? vCard[vCard::Property::PHOTO_JPEG]
                           : vCard[vCard::Property::PHOTO_PNG];
    return photo;
}

api::contact::Info
buildContactFromProfile(const QString& accountId,
                        const QString& peer_uri,
                        const api::profile::Type& type)
{
    lrc::api::profile::Info profileInfo;
    profileInfo.uri = peer_uri;
    profileInfo.type = type;
    auto accountLocalPath = getPath() + accountId + "/";
    QString b64filePath;
    b64filePath = profileVcardPath(accountId, peer_uri);
    QFile file(b64filePath);
    if (!file.open(QIODevice::ReadOnly)) {
        // try non-base64 path
        QString filePath = accountLocalPath + "profiles/" + peer_uri + ".vcf";
        file.setFileName(filePath);
        if (!file.open(QIODevice::ReadOnly)) {
            return {profileInfo, "", true, false};
        }
        // rename it
        qWarning().noquote() << "Renaming profile: " << filePath;
        file.rename(b64filePath);
        // reopen it
        if (!file.open(QIODevice::ReadOnly)) {
            qWarning().noquote() << "Can't open file: " << b64filePath;
            return {profileInfo, "", true, false};
        }
    }
    const auto vCard = lrc::vCard::utils::toHashMap(file.readAll());
    const auto alias = vCard[vCard::Property::FORMATTED_NAME];
    if (lrc::api::Lrc::cacheAvatars.load()) {
        for (const auto& key : vCard.keys()) {
            if (key.contains("PHOTO"))
                profileInfo.avatar = vCard[key];
        }
    }
    profileInfo.alias = alias;
    return {profileInfo, "", type == api::profile::Type::JAMI, false};
}

QString
avatar(const QString& accountId, const QString& contactId)
{
    if (contactId.isEmpty())
        return getAccountAvatar(accountId);
    auto accountLocalPath = getPath() + accountId + "/";
    QString b64filePath;
    b64filePath = profileVcardPath(accountId, contactId);
    QFile file(b64filePath);
    if (!file.open(QIODevice::ReadOnly)) {
        return {};
    }
    const auto vCard = lrc::vCard::utils::toHashMap(file.readAll());
    for (const auto& key : vCard.keys()) {
        if (key.contains("PHOTO"))
            return vCard[key];
    }
    return {};
}

VectorString
getAllConversations(Database& db)
{
    return db.select("id", "conversations", {}, {}).payloads;
}

VectorString
getConversationsBetween(Database& db, const QString& peer1_uri, const QString& peer2_uri)
{
    auto conversationsForPeer1 = getConversationsWithPeer(db, peer1_uri);
    std::sort(conversationsForPeer1.begin(), conversationsForPeer1.end());
    auto conversationsForPeer2 = getConversationsWithPeer(db, peer2_uri);
    std::sort(conversationsForPeer2.begin(), conversationsForPeer2.end());
    VectorString common;

    std::set_intersection(conversationsForPeer1.begin(),
                          conversationsForPeer1.end(),
                          conversationsForPeer2.begin(),
                          conversationsForPeer2.end(),
                          std::back_inserter(common));
    return common;
}

QString
beginConversationWithPeer(Database& db,
                          const QString& peer_uri,
                          const bool isOutgoing,
                          time_t timestamp)
{
    // Add conversation between account and profile
    auto newConversationsId = db.select("IFNULL(MAX(id), 0) + 1", "conversations", "1=1", {})
                                  .payloads[0];
    db.insertInto("conversations",
                  {{":id", "id"}, {":participant", "participant"}},
                  {{":id", newConversationsId}, {":participant", peer_uri}});
    api::interaction::Info msg {isOutgoing ? "" : peer_uri,
                                {},
                                timestamp ? timestamp : std::time(nullptr),
                                0,
                                api::interaction::Type::CONTACT,
                                isOutgoing ? api::interaction::Status::SUCCESS
                                           : api::interaction::Status::UNKNOWN,
                                isOutgoing};
    // Add first interaction
    addMessageToConversation(db, newConversationsId, msg);
    return newConversationsId;
}

void
getHistory(Database& db, api::conversation::Info& conversation, const QString& localUri)
{
    auto interactionsResult
        = db.select("id, author, body, timestamp, type, status, is_read, extra_data",
                    "interactions",
                    "conversation=:conversation",
                    {{":conversation", conversation.uid}});
    auto nCols = 8;
    if (interactionsResult.nbrOfCols == nCols) {
        auto payloads = interactionsResult.payloads;
        for (decltype(payloads.size()) i = 0; i < payloads.size(); i += nCols) {
            QString durationString;
            auto extra_data_str = payloads[i + 7];
            if (!extra_data_str.isEmpty()) {
                auto jsonData = JSONFromString(extra_data_str);
                durationString = readJSONValue(jsonData, "duration");
            }
            auto body = payloads[i + 2];
            auto type = api::interaction::to_type(payloads[i + 4]);
            std::time_t duration = durationString.isEmpty()
                                       ? 0
                                       : std::stoi(durationString.toStdString());
            auto status = api::interaction::to_status(payloads[i + 5]);
            if (type == api::interaction::Type::CALL) {
                body = getCallInteractionStringNonSwarm(payloads[i + 1] == localUri, duration);
            } else if (type == api::interaction::Type::CONTACT) {
                body = getContactInteractionString(payloads[i + 1], status);
            }
            auto msg = api::interaction::Info({payloads[i + 1],
                                               body,
                                               std::stoi(payloads[i + 3].toStdString()),
                                               duration,
                                               type,
                                               status,
                                               (payloads[i + 6] == "1" ? true : false)});
            conversation.interactions->emplace(payloads[i], std::move(msg));
            conversation.lastMessageUid = payloads[i];
            if (status != api::interaction::Status::DISPLAYED || !payloads[i + 1].isEmpty()) {
                continue;
            }
            conversation.interactions->setRead(conversation.participants.front().uri, payloads[i]);
        }
    }
}

QString
addMessageToConversation(Database& db,
                         const QString& conversationId,
                         const api::interaction::Info& msg)
{
    return db.insertInto("interactions",
                         {{":author", "author"},
                          {":conversation", "conversation"},
                          {":timestamp", "timestamp"},
                          {":body", "body"},
                          {":type", "type"},
                          {":status", "status"},
                          {":is_read", "is_read"}},
                         {{":author", msg.authorUri},
                          {":conversation", conversationId},
                          {":timestamp", toQString(msg.timestamp)},
                          {":body", msg.body},
                          {":type", to_string(msg.type)},
                          {":status", to_string(msg.status)},
                          {":is_read", msg.isRead ? "1" : "0"}});
}

QString
addOrUpdateMessage(Database& db,
                   const QString& conversationId,
                   const api::interaction::Info& msg,
                   const QString& daemonId)
{
    // Check if profile is already present.
    auto msgAlreadyExists = db.select("id",
                                      "interactions",
                                      "author=:author AND daemon_id=:daemon_id",
                                      {{":author", msg.authorUri}, {":daemon_id", daemonId}})
                                .payloads;
    if (msgAlreadyExists.empty()) {
        auto extra_data_JSON = JSONFromString("");
        writeJSONValue(extra_data_JSON, "duration", QString::number(msg.duration));
        auto extra_data = stringFromJSON(extra_data_JSON);
        return db.insertInto("interactions",
                             {{":author", "author"},
                              {":conversation", "conversation"},
                              {":timestamp", "timestamp"},
                              {":body", "body"},
                              {":type", "type"},
                              {":status", "status"},
                              {":daemon_id", "daemon_id"},
                              {":extra_data", "extra_data"}},
                             {{":author", msg.authorUri.isEmpty() ? "" : msg.authorUri},
                              {":conversation", conversationId},
                              {":timestamp", toQString(msg.timestamp)},
                              {msg.body.isEmpty() ? "" : ":body", msg.body},
                              {":type", to_string(msg.type)},
                              {daemonId.isEmpty() ? "" : ":daemon_id", daemonId},
                              {":status", to_string(msg.status)},
                              {extra_data.isEmpty() ? "" : ":extra_data", extra_data}});
    } else {
        // already exists @ id(msgAlreadyExists[0])
        auto id = msgAlreadyExists[0];
        QString extra_data;
        if (msg.type == api::interaction::Type::CALL) {
            auto duration = std::max(msg.duration, static_cast<std::time_t>(0));
            auto extra_data_str = getInteractionExtraDataById(db, id);
            auto extra_data_JSON = JSONFromString(extra_data_str);
            writeJSONValue(extra_data_JSON, "duration", QString::number(duration));
            extra_data = stringFromJSON(extra_data_JSON);
        }
        db.update("interactions",
                  {"body=:body, extra_data=:extra_data"},
                  {{msg.body.isEmpty() ? "" : ":body", msg.body},
                   {extra_data.isEmpty() ? "" : ":extra_data", extra_data}},
                  "id=:id",
                  {{":id", id}});
        return id;
    }
}

QString
addDataTransferToConversation(Database& db,
                              const QString& conversationId,
                              const api::datatransfer::Info& infoFromDaemon)
{
    auto convId = conversationId.isEmpty() ? NULL : conversationId;
    return db.insertInto("interactions",
                         {{":author", "author"},
                          {":conversation", "conversation"},
                          {":timestamp", "timestamp"},
                          {":body", "body"},
                          {":type", "type"},
                          {":status", "status"},
                          {":is_read", "is_read"},
                          {":daemon_id", "daemon_id"}},
                         {{":author", infoFromDaemon.isOutgoing ? "" : infoFromDaemon.peerUri},
                          {":conversation", convId},
                          {":timestamp", toQString(std::time(nullptr))},
                          {":body", infoFromDaemon.path},
                          {":type", "DATA_TRANSFER"},
                          {":status", "TRANSFER_CREATED"},
                          {":is_read", "0"},
                          {":daemon_id", infoFromDaemon.uid}});
}

void
addDaemonMsgId(Database& db, const QString& interactionId, const QString& daemonId)
{
    db.update("interactions",
              "daemon_id=:daemon_id",
              {{":daemon_id", daemonId}},
              "id=:id",
              {{":id", interactionId}});
}

QString
getDaemonIdByInteractionId(Database& db, const QString& id)
{
    auto ids = db.select("daemon_id", "interactions", "id=:id", {{":id", id}}).payloads;
    return ids.empty() ? "" : ids[0];
}

QString
getInteractionIdByDaemonId(Database& db, const QString& daemon_id)
{
    auto ids = db.select("id", "interactions", "daemon_id=:daemon_id", {{":daemon_id", daemon_id}})
                   .payloads;
    return ids.empty() ? "" : ids[0];
}

void
updateDataTransferInteractionForDaemonId(Database& db,
                                         const QString& daemonId,
                                         api::interaction::Info& interaction)
{
    auto result = db.select("body, status",
                            "interactions",
                            "daemon_id=:daemon_id",
                            {{":daemon_id", daemonId}})
                      .payloads;
    if (result.size() < 2) {
        return;
    }
    auto body = result[0];
    auto status = api::interaction::to_status(result[1]);
    interaction.body = body;
    interaction.status = status;
}

QString
getInteractionExtraDataById(Database& db, const QString& id, const QString& key)
{
    auto extra_datas = db.select("extra_data", "interactions", "id=:id", {{":id", id}}).payloads;
    if (key.isEmpty()) {
        return extra_datas.empty() ? "" : extra_datas[0];
    }
    QString value;
    if (!extra_datas[0].isEmpty()) {
        value = readJSONValue(JSONFromString(extra_datas[0]), key);
    }
    return value;
}

void
updateInteractionBody(Database& db, const QString& id, const QString& newBody)
{
    db.update("interactions", "body=:body", {{":body", newBody}}, "id=:id", {{":id", id}});
}

void
updateInteractionStatus(Database& db, const QString& id, api::interaction::Status newStatus)
{
    db.update("interactions",
              {"status=:status"},
              {{":status", api::interaction::to_string(newStatus)}},
              "id=:id",
              {{":id", id}});
}

void
setInteractionRead(Database& db, const QString& id)
{
    db.update("interactions", {"is_read=:is_read"}, {{":is_read", "1"}}, "id=:id", {{":id", id}});
}

QString
conversationIdFromInteractionId(Database& db, const QString& interactionId)
{
    auto result = db.select("conversation", "interactions", "id=:id", {{":id", interactionId}});
    if (result.nbrOfCols == 1 && result.payloads.size()) {
        return result.payloads[0];
    }
    return {};
}

void
clearHistory(Database& db, const QString& conversationId)
{
    try {
        db.deleteFrom("interactions",
                      "conversation=:conversation",
                      {{":conversation", conversationId}});
    } catch (Database::QueryDeleteError& e) {
        qWarning() << "deleteFrom error: " << e.details();
    }
}

void
clearInteractionFromConversation(Database& db,
                                 const QString& conversationId,
                                 const QString& interactionId)
{
    try {
        db.deleteFrom("interactions",
                      "conversation=:conversation AND id=:id",
                      {{":conversation", conversationId}, {":id", interactionId}});
    } catch (Database::QueryDeleteError& e) {
        qWarning() << "deleteFrom error: " << e.details();
    }
}

void
clearAllHistory(Database& db)
{
    try {
        db.deleteFrom("interactions", "1=1", {});
    } catch (Database::QueryDeleteError& e) {
        qWarning() << "deleteFrom error: " << e.details();
    }
}

void
deleteObsoleteHistory(Database& db, long int date)
{
    try {
        db.deleteFrom("interactions", "timestamp<=:date", {{":date", QString::number(date)}});
    } catch (Database::QueryDeleteError& e) {
        qWarning() << "deleteFrom error: " << e.details();
    }
}

void
removeContactConversations(Database& db, const QString& contactUri)
{
    // Get common conversations
    auto conversations = getConversationsWithPeer(db, contactUri);
    // Remove conversations + interactions
    try {
        for (const auto& conversationId : conversations) {
            // Remove conversation
            db.deleteFrom("conversations", "id=:id", {{":id", conversationId}});
            // clear History
            db.deleteFrom("interactions", "conversation=:id", {{":id", conversationId}});
        }
    } catch (Database::QueryDeleteError& e) {
        qWarning() << "deleteFrom error: " << e.details();
    }
}

int
countUnreadFromInteractions(Database& db, const QString& conversationId)
{
    return db.count("is_read",
                    "interactions",
                    "is_read=:is_read AND conversation=:id",
                    {{":is_read", "0"}, {":id", conversationId}});
}

uint64_t
getLastTimestamp(Database& db)
{
    auto timestamps = db.select("MAX(timestamp)", "interactions", "1=1", {}).payloads;
    auto result = std::time(nullptr);
    try {
        if (!timestamps.empty() && !timestamps[0].isEmpty()) {
            result = std::stoull(timestamps[0].toStdString());
        }
    } catch (const std::out_of_range& e) {
        qDebug() << "storage::getLastTimestamp, stoull throws an out_of_range exception: "
                 << e.what();
    } catch (const std::invalid_argument& e) {
        qDebug() << "storage::getLastTimestamp, stoull throws an invalid_argument exception: "
                 << e.what();
    }
    return result;
}

//================================================================================
// This section provides migration helpers from ring.db
// to per-account databases yielding a file structure like:
//
// { local_storage } / jami
// └──{ account_id }
// ├── config.yml
// ├── contacts
// ├── export.gz
// ├── incomingTrustRequests
// ├── knownDevicesNames
// ├── history.db < --conversations and interactions database
// ├── profile.vcf < --account vcard
// ├── profiles < --account contact vcards
// │   │──{ contact_uri }.vcf
// │   └── ...
// ├── ring_device.crt
// └── ring_device.key
//================================================================================
namespace migration {

enum class msgFlag {
    IS_INCOMING,
    IS_OUTGOING,
    IS_CONTACT_ADDED,
    IS_INVITATION_RECEIVED,
    IS_INVITATION_ACCEPTED,
    IS_TEXT
};

QString profileToVcard(const lrc::api::profile::Info&, const QString&);
uint64_t getTimeFromTimeStr(const QString&) noexcept;
std::pair<msgFlag, uint64_t> migrateMessageBody(const QString&, const lrc::api::interaction::Type&);
VectorString getPeerParticipantsForConversationId(lrc::Database&, const QString&, const QString&);
void migrateAccountDb(const QString&,
                      std::shared_ptr<lrc::Database>,
                      std::shared_ptr<lrc::Database>);

namespace interaction {

static inline api::interaction::Type
to_type(const QString& type)
{
    if (type == "TEXT")
        return api::interaction::Type::TEXT;
    else if (type == "CALL")
        return api::interaction::Type::CALL;
    else if (type == "CONTACT")
        return api::interaction::Type::CONTACT;
    else if (type == "OUTGOING_DATA_TRANSFER")
        return api::interaction::Type::DATA_TRANSFER;
    else if (type == "INCOMING_DATA_TRANSFER")
        return api::interaction::Type::DATA_TRANSFER;
    else
        return api::interaction::Type::INVALID;
}

static inline QString
to_migrated_status_string(const QString& status)
{
    if (status == "FAILED")
        return "FAILURE";
    else if (status == "SUCCEED")
        return "SUCCESS";
    else if (status == "READ")
        return "SUCCESS";
    else if (status == "UNREAD")
        return "SUCCESS";
    else
        return status;
}

} // namespace interaction

QString
profileToVcard(const api::profile::Info& profileInfo, const QString& accountId = {})
{
    using namespace api;
    bool compressedImage = std::strncmp(profileInfo.avatar.toStdString().c_str(), "/9g=", 4) == 0;
    ;
    QString vCardStr = vCard::Delimiter::BEGIN_TOKEN;
    vCardStr += vCard::Delimiter::END_LINE_TOKEN;
    vCardStr += vCard::Property::VERSION;
    vCardStr += ":2.1";
    vCardStr += vCard::Delimiter::END_LINE_TOKEN;
    if (!accountId.isEmpty()) {
        vCardStr += vCard::Property::UID;
        vCardStr += ":";
        vCardStr += accountId;
        vCardStr += vCard::Delimiter::END_LINE_TOKEN;
    }
    vCardStr += vCard::Property::FORMATTED_NAME;
    vCardStr += ":";
    vCardStr += profileInfo.alias;
    vCardStr += vCard::Delimiter::END_LINE_TOKEN;
    if (profileInfo.type == profile::Type::JAMI) {
        vCardStr += vCard::Property::TELEPHONE;
        vCardStr += ":";
        vCardStr += vCard::Delimiter::SEPARATOR_TOKEN;
        vCardStr += "other:ring:";
        vCardStr += profileInfo.uri;
        vCardStr += vCard::Delimiter::END_LINE_TOKEN;
    } else {
        vCardStr += vCard::Property::TELEPHONE;
        vCardStr += profileInfo.uri;
        vCardStr += vCard::Delimiter::END_LINE_TOKEN;
    }
    vCardStr += vCard::Property::PHOTO;
    vCardStr += vCard::Delimiter::SEPARATOR_TOKEN;
    vCardStr += "ENCODING=BASE64";
    vCardStr += vCard::Delimiter::SEPARATOR_TOKEN;
    vCardStr += compressedImage ? "TYPE=JPEG:" : "TYPE=PNG:";
    vCardStr += profileInfo.avatar;
    vCardStr += vCard::Delimiter::END_LINE_TOKEN;
    vCardStr += vCard::Delimiter::END_TOKEN;
    return vCardStr;
}

uint64_t
getTimeFromTimeStr(const QString& str) noexcept
{
    uint64_t minutes = 0, seconds = 0;
    std::string timeStr = str.toStdString();
    std::size_t delimiterPos = timeStr.find(":");
    if (delimiterPos != std::string::npos) {
        try {
            minutes = std::stoull(timeStr.substr(0, delimiterPos));
            seconds = std::stoull(timeStr.substr(delimiterPos + 1));
        } catch (const std::exception&) {
            return 0;
        }
    }
    return minutes * 60 + seconds;
}

std::pair<msgFlag, uint64_t>
migrateMessageBody(const QString& body, const api::interaction::Type& type)
{
    uint64_t duration {0};
    // check in english and local to determine the direction of the call
    static QString emo = "Missed outgoing call";
    static QString lmo = QObject::tr("Missed outgoing call");
    static QString eo = "Outgoing call";
    static QString lo = QObject::tr("Outgoing call");
    static QString eca = "Contact added";
    static QString lca = QObject::tr("Contact added");
    static QString eir = "Invitation received";
    static QString lir = QObject::tr("Invitation received");
    static QString eia = "Invitation accepted";
    static QString lia = QObject::tr("Invitation accepted");
    auto strBody = body.toStdString();
    switch (type) {
    case api::interaction::Type::CALL: {
        bool en_missedOut = body.contains(emo);
        bool en_out = body.contains(eo);
        bool loc_missedOut = body.contains(lmo);
        bool loc_out = body.contains(lo);
        bool outgoingCall = en_missedOut || en_out || loc_missedOut || loc_out;
        std::size_t dashPos = strBody.find("-");
        if (dashPos != std::string::npos) {
            duration = getTimeFromTimeStr(toQString(strBody.substr(dashPos + 2)));
        }
        return std::make_pair(msgFlag(outgoingCall), duration);
    } break;
    case api::interaction::Type::CONTACT:
        if (body.contains(eca) || body.contains(lca)) {
            return std::make_pair(msgFlag::IS_CONTACT_ADDED, 0);
        } else if (body.contains(eir) || body.contains(lir)) {
            return std::make_pair(msgFlag::IS_INVITATION_RECEIVED, 0);
        } else if (body.contains(eia) || body.contains(lia)) {
            return std::make_pair(msgFlag::IS_INVITATION_ACCEPTED, 0);
        }
        break;
    case api::interaction::Type::INVALID:
    case api::interaction::Type::TEXT:
    case api::interaction::Type::DATA_TRANSFER:
    case api::interaction::Type::COUNT__:
    default:
        return std::make_pair(msgFlag::IS_TEXT, 0);
    }
    return std::make_pair(msgFlag::IS_OUTGOING, 0);
}

VectorString
getPeerParticipantsForConversationId(Database& db,
                                     const QString& profileId,
                                     const QString& conversationId)
{
    return db
        .select("participant_id",
                "conversations",
                "id=:id AND participant_id!=:participant_id",
                {{":id", conversationId}, {":participant_id", profileId}})
        .payloads;
}

void
migrateAccountDb(const QString& accountId,
                 std::shared_ptr<Database> db,
                 std::shared_ptr<Database> legacyDb)
{
    using namespace lrc::api;
    using namespace migration;

    auto accountLocalPath = getPath() + accountId + "/";

    using namespace libjami::Account;
    MapStringString accountDetails = ConfigurationManager::instance().getAccountDetails(
        accountId.toStdString().c_str());
    bool isRingAccount = accountDetails[ConfProperties::TYPE] == "RING";
    std::map<QString, QString> profileIdUriMap;
    std::map<QString, QString> convIdPeerUriMap;
    QString accountProfileId;

    // 1. profiles_accounts
    // migrate account's avatar/alias from profiles table to {data_dir}/profile.vcf
    QString accountUri;
    if (isRingAccount) {
        accountUri = accountDetails[libjami::Account::ConfProperties::USERNAME].contains("ring:")
                         ? QString(accountDetails[libjami::Account::ConfProperties::USERNAME])
                               .remove(QString("ring:"))
                         : accountDetails[libjami::Account::ConfProperties::USERNAME];
    } else {
        accountUri = accountDetails[libjami::Account::ConfProperties::USERNAME];
    }

    auto accountProfileIds = legacyDb
                                 ->select("profile_id",
                                          "profiles_accounts",
                                          "account_id=:account_id AND is_account=:is_account",
                                          {{":account_id", accountId}, {":is_account", "true"}})
                                 .payloads;
    if (accountProfileIds.size() != 1) {
        return;
    }
    accountProfileId = accountProfileIds[0];
    auto accountProfile
        = legacyDb->select("photo, alias", "profiles", "id=:id", {{":id", accountProfileId}})
              .payloads;
    profile::Info accountProfileInfo;
    // if we can not find the uri in the database
    // (in the case of poorly kept SIP account uris),
    // than we cannot migrate the conversations and vcard
    if (!accountProfile.empty()) {
        accountProfileInfo = {accountUri,
                              accountProfile[0],
                              accountProfile[1],
                              isRingAccount ? profile::Type::JAMI : profile::Type::SIP};
    }
    auto accountVcard = profileToVcard(accountProfileInfo, accountId);
    QDir dir;
    if (!dir.exists(accountLocalPath)) {
        dir.mkpath(accountLocalPath);
    }
    auto profileFilePath = accountLocalPath + "profile.vcf";
    QFile file(profileFilePath);
    if (!file.open(QIODevice::WriteOnly)) {
        throw std::runtime_error("Can't open file: " + profileFilePath.toStdString());
    }
    QTextStream(&file) << accountVcard;

    // 2. profiles
    // migrate profiles from profiles table to {data_dir}/{uri}.vcf
    // - for JAMI, the scheme and the hostname is omitted
    // - for SIP, the uri is must be stripped of prefix and port
    // e.g. 3d1112ab2bb089370c0744a44bbbb0786418d40b.vcf
    //      username.vcf or username@hostname.vcf

    // only select non-account profiles
    auto profileIds = legacyDb
                          ->select("profile_id",
                                   "profiles_accounts",
                                   "account_id=:account_id AND is_account=:is_account",
                                   {{":account_id", accountId}, {":is_account", "false"}})
                          .payloads;
    for (const auto& profileId : profileIds) {
        auto profile = legacyDb
                           ->select("uri, alias, photo, type",
                                    "profiles",
                                    "id=:id",
                                    {{":id", profileId}})
                           .payloads;
        if (profile.empty()) {
            continue;
        }
        profile::Info profileInfo {profile[0], profile[2], profile[1]};
        auto uri = URI(profile[0]);
        auto profileUri = uri.userinfo();
        if (!isRingAccount && uri.hasHostname()) {
            profileUri += "@" + uri.hostname();
        }
        // insert into map for use during the conversations table migration
        profileIdUriMap.insert(std::make_pair(profileId, profileUri));
        auto vcard = profileToVcard(profileInfo);
        // make sure the directory exists
        QDir dir(accountLocalPath + "profiles");
        if (!dir.exists())
            dir.mkpath(".");
        profileFilePath = accountLocalPath + "profiles/" + profileUri + ".vcf";
        QFile file(profileFilePath);
        // if we catch duplicates here, skip the profile because
        // the previous db structure does not guarantee unique uris
        if (file.exists()) {
            qWarning() << "Profile file already exits: " << profileFilePath;
            continue;
        }
        if (!file.open(QIODevice::WriteOnly)) {
            qWarning() << "Can't open file: " << profileFilePath;
            continue;
        }
        QTextStream(&file) << vcard;
    }

    // 3. conversations
    // migrate old conversations table ==> new conversations table
    // a) participant_id INTEGER becomes participant TEXT (the uri of the participant)
    //    use the selected non-account profiles
    auto conversationIds = legacyDb
                               ->select("id",
                                        "conversations",
                                        "participant_id=:participant_id",
                                        {{":participant_id", accountProfileId}})
                               .payloads;
    if (conversationIds.empty()) {
        return;
    }
    for (auto conversationId : conversationIds) {
        // only one peer pre-groupchat
        auto peerProfileId = getPeerParticipantsForConversationId(*legacyDb,
                                                                  accountProfileId,
                                                                  conversationId);
        if (peerProfileId.empty()) {
            continue;
        }
        auto it = profileIdUriMap.find(peerProfileId.at(0));
        // we cannot insert in the conversations table without a uri
        if (it == profileIdUriMap.end()) {
            continue;
        }
        convIdPeerUriMap.insert(std::make_pair(conversationId, it->second));
        try {
            db->insertInto("conversations",
                           {{":id", "id"}, {":participant", "participant"}},
                           {{":id", conversationId}, {":participant", it->second}});
        } catch (const std::runtime_error& e) {
            qWarning() << "Couldn't migrate conversation: " << e.what();
            continue;
        }
    }

    // 4. interactions
    auto allInteractions = legacyDb->select("account_id, author_id, conversation_id, \
         timestamp, body, type, status, daemon_id",
                                            "interactions",
                                            "account_id=:account_id",
                                            {{":account_id", accountProfileId}});
    auto interactionIt = allInteractions.payloads.begin();
    while (interactionIt != allInteractions.payloads.end()) {
        auto author_id = *(interactionIt + 1);
        auto convId = *(interactionIt + 2);
        auto timestamp = *(interactionIt + 3);
        auto body = *(interactionIt + 4);
        auto type = interaction::to_type(*(interactionIt + 5));
        auto statusStr = *(interactionIt + 6);
        auto daemonId = *(interactionIt + 7);

        auto it = profileIdUriMap.find(author_id);
        if (it == profileIdUriMap.end() && author_id != accountProfileId) {
            std::advance(interactionIt, allInteractions.nbrOfCols);
            continue;
        }
        // migrate body+type ==> msgFlag+duration
        auto migratedMsg = migrateMessageBody(body, type);
        QString profileUri = it == profileIdUriMap.end() ? "" : it->second;
        // clear author uri if outgoing
        switch (migratedMsg.first) {
        case msgFlag::IS_OUTGOING:
        case msgFlag::IS_CONTACT_ADDED:
            profileUri.clear();
            break;
        case msgFlag::IS_INCOMING:
        case msgFlag::IS_INVITATION_RECEIVED:
        case msgFlag::IS_INVITATION_ACCEPTED: {
            // try to set profile uri using the conversation id
            auto it = convIdPeerUriMap.find(convId);
            if (it == convIdPeerUriMap.end()) {
                std::advance(interactionIt, allInteractions.nbrOfCols);
                continue;
            }
            profileUri = it->second;
            break;
        }
        case msgFlag::IS_TEXT:
        default:
            break;
        }
        // Set all read, call and datatransfer, and contact added
        // interactions to a read state
        bool is_read = statusStr != "UNREAD" || type == api::interaction::Type::CALL
                       || type == api::interaction::Type::CONTACT;
        // migrate status
        if (migratedMsg.first == msgFlag::IS_INVITATION_RECEIVED) {
            statusStr = "UNKNOWN";
        }
        QString extra_data = migratedMsg.second == 0
                                 ? ""
                                 : JSONStringFromInitList(
                                     {qMakePair(QString("duration"),
                                                QJsonValue(QString::number(migratedMsg.second)))});
        if (accountUri == profileUri)
            profileUri.clear();
        auto typeStr = api::interaction::to_string(type);
        try {
            db->insertInto("interactions",
                           {{":author", "author"},
                            {":conversation", "conversation"},
                            {":timestamp", "timestamp"},
                            {":body", "body"},
                            {":type", "type"},
                            {":status", "status"},
                            {":is_read", "is_read"},
                            {":daemon_id", "daemon_id"},
                            {":extra_data", "extra_data"}},
                           {{":author", profileUri},
                            {":conversation", convId},
                            {":timestamp", timestamp},
                            {migratedMsg.first != msgFlag::IS_TEXT ? "" : ":body", body},
                            {":type", api::interaction::to_string(type)},
                            {":status", interaction::to_migrated_status_string(statusStr)},
                            {":is_read", is_read ? "1" : "0"},
                            {daemonId.isEmpty() ? "" : ":daemon_id", daemonId},
                            {extra_data.isEmpty() ? "" : ":extra_data", extra_data}});
        } catch (const std::runtime_error& e) {
            qWarning() << e.what();
        }
        std::advance(interactionIt, allInteractions.nbrOfCols);
    }
    qDebug() << "Done";
}

} // namespace migration

std::vector<std::shared_ptr<Database>>
migrateIfNeeded(const QStringList& accountIds, MigrationCb& willMigrateCb, MigrationCb& didMigrateCb)
{
    using namespace lrc::api;
    using namespace migration;

    std::vector<std::shared_ptr<Database>> dbs(accountIds.size());

    if (!accountIds.size()) {
        qDebug() << "No accounts to migrate";
        return dbs;
    }

    auto appPath = getPath();

    // ring -> jami path migration
    QDir dataDir(appPath);
    // create data directory if not created yet
    dataDir.mkpath(appPath);
    QDir oldDataDir(appPath);
    oldDataDir.cdUp();
    oldDataDir = oldDataDir.absolutePath()
#if defined(_WIN32)
                 + "/Savoir-faire Linux/Ring";
#elif defined(__APPLE__)
                 + "/ring";
#else
                 + "/gnome-ring";
#endif
    QStringList filesList = oldDataDir.entryList();
    QString filename;
    QDir dir;
    bool success = true;
    Q_FOREACH (filename, filesList) {
        qDebug() << "Migrate " << oldDataDir.absolutePath() << "/" << filename << " to "
                 << dataDir.absolutePath() + "/" + filename;
        if (filename != "." && filename != "..") {
            success &= dir.rename(oldDataDir.absolutePath() + "/" + filename,
                                  dataDir.absolutePath() + "/" + filename);
        }
    }
    if (success) {
        // Remove old directory if the migration is successful.
#if defined(_WIN32)
        oldDataDir.cdUp();
#endif
        oldDataDir.removeRecursively();
    }

    bool needsMigration = false;
    std::map<QString, bool> hasMigratedData;
    for (const auto& accountId : accountIds) {
        auto hasMigratedDb = QFile(appPath + accountId + "/history.db").exists()
                             && !QFile(appPath + accountId + "/history.db-journal").exists();
        hasMigratedData.insert(std::make_pair(accountId, hasMigratedDb));
        needsMigration |= !hasMigratedDb;
    }
    if (!needsMigration) {
        // if there's any lingering pre-migration data, remove it
        QFile(dataDir.absoluteFilePath("ring.db")).remove();
        QDir(dataDir.absoluteFilePath("text/")).removeRecursively();
        QDir(dataDir.absoluteFilePath("profiles/")).removeRecursively();
        QDir(dataDir.absoluteFilePath("peer_profiles/")).removeRecursively();
        qDebug() << "No migration required";
        return dbs;
    }

    // A fairly long migration may now occur
    std::thread migrateThread(
        [&appPath, &accountIds, &dbs, &didMigrateCb, &dataDir, &hasMigratedData] {
            // 1. migrate old lrc -> new lrc if needed
            // 2. migrate new lrc db version 1 -> db version 1.1 if needed
            // the destructor of LegacyDatabase will remove 'ring.db' and clean out
            // old lrc files
            std::shared_ptr<Database> legacyDb;
            try {
                legacyDb = lrc::DatabaseFactory::create<LegacyDatabase>(appPath);
            } catch (const std::runtime_error& e) {
                qDebug() << "Exception while attempting to load legacy database: " << e.what();
                if (didMigrateCb)
                    didMigrateCb();
                return;
            }

            // attempt to make a backup of ring.db
            {
                QFile dbFile(dataDir.absoluteFilePath("ring.db"));
                if (dbFile.open(QIODevice::ReadOnly)) {
                    dbFile.copy(appPath + "ring.db.bak");
                }
            }

            // 3. migrate db version 1.1 -> per account dbs version 1
            int index = 0;
            for (const auto& accountId : accountIds) {
                if (hasMigratedData.at(accountId)) {
                    index++;
                    continue;
                }
                qDebug() << "Migrating account: " << accountId << "...";
                // try to remove the transaction journal from a failed migration
                QFile(appPath + accountId + "/history.db-journal").remove();
                try {
                    QSqlDatabase::database().transaction();
                    auto dbName = QString::fromStdString(accountId.toStdString() + "/history");
                    dbs.at(index) = lrc::DatabaseFactory::create<Database>(dbName, appPath);
                    auto& db = dbs.at(index++);
                    migration::migrateAccountDb(accountId, db, legacyDb);
                    QSqlDatabase::database().commit();
                } catch (const std::runtime_error& e) {
                    qWarning().noquote() << "Could not migrate database for account: " << accountId
                                         << "\n " << e.what();
                    QSqlDatabase::database().rollback();
                }
            }

            // done
            if (didMigrateCb)
                didMigrateCb();
        });

    // if willMigrateCb blocks, it must be unblocked by didMigrateCb
    if (willMigrateCb)
        willMigrateCb();

    migrateThread.join();

    return dbs;
}

} // namespace storage

} // namespace authority

} // namespace lrc
