/****************************************************************************
 *   Copyright (C) 2017-2025 Savoir-faire Linux Inc.                        *
 *                                                                          *
 *   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 "api/conversationmodel.h"

// LRC
#include "api/lrc.h"
#include "api/behaviorcontroller.h"
#include "api/contactmodel.h"
#include "api/callmodel.h"
#include "api/accountmodel.h"
#include "api/account.h"
#include "api/call.h"
#include "api/datatransfer.h"
#include "api/datatransfermodel.h"
#include "callbackshandler.h"
#include "containerview.h"
#include "authority/storagehelper.h"
#include "uri.h"

// Dbus
#include "dbus/configurationmanager.h"
#include "dbus/callmanager.h"

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

// Qt
#include <QtCore/QTimer>
#include <QFileInfo>

// std
#include <algorithm>
#include <mutex>
#include <regex>
#include <sstream>

namespace lrc {

using namespace authority;
using namespace api;

class ConversationModelPimpl : public QObject
{
    Q_OBJECT
public:
    ConversationModelPimpl(const ConversationModel& linked,
                           Lrc& lrc,
                           Database& db,
                           const CallbacksHandler& callbacksHandler,
                           const BehaviorController& behaviorController);

    ~ConversationModelPimpl();

    using FilterPredicate = std::function<bool(const conversation::Info& convInfo)>;

    /**
     * return a conversation index from conversations or -1 if no index is found.
     * @param uid of the contact to search.
     * @return an int.
     */
    int indexOf(const QString& uid) const;

    /**
     * return a reference to a conversation with given filter
     * @param pred a unary comparison predicate with which to find the conversation
     * @param searchResultIncluded if need to search in contacts and userSearch
     * @return a reference to a conversation with given uid
     */
    std::reference_wrapper<conversation::Info> getConversation(const FilterPredicate& pred,
                                                               const bool searchResultIncluded = false) const;

    /**
     * return a reference to a conversation with given uid.
     * @param conversation uid.
     * @param searchResultIncluded if need to search in contacts and userSearch.
     * @return a reference to a conversation with the given uid.
     */
    std::reference_wrapper<conversation::Info> getConversationForUid(const QString& uid,
                                                                     const bool searchResultIncluded = false) const;

    /**
     * return a reference to a conversation with participant.
     * @param participant uri.
     * @param searchResultIncluded if need to search in contacts and userSearch.
     * @return a reference to a conversation with the given peer uri.
     * @warning we could  have multiple swarm conversations for the same peer. This function will
     * return an active one-to-one conversation.
     */
    std::reference_wrapper<conversation::Info> getConversationForPeerUri(const QString& uri,
                                                                         const bool searchResultIncluded = false) const;
    /**
     * return a vector of conversation indices for the given contact uri empty
     * if no index is found
     * @param uri of the contact to search
     * @return an vector of indices
     */
    std::vector<int> getIndicesForContact(const QString& uri) const;
    /**
     * Initialize conversations_ and filteredConversations_
     */
    void initConversations();
    /**
     * Filter all conversations
     */
    bool filter(const conversation::Info& conv);
    /**
     * Sort conversation by last action
     */
    bool sort(const conversation::Info& convA, const conversation::Info& convB);
    /**
     * Call contactModel.addContact if necessary
     * @param contactUri
     */
    void sendContactRequest(const QString& contactUri);
    /**
     * Add a conversation with contactUri
     * @param convId
     * @param contactUri
     */
    void addConversationWith(const QString& convId, const QString& contactUri, bool isRequest);
    /**
     * Add a swarm conversation to conversation list
     * @param convId
     */
    void addSwarmConversation(const QString& convId);
    /**
     * Add call interaction for conversation with callId
     * @param callId
     * @param duration
     */
    void addOrUpdateCallMessage(const QString& callId,
                                const QString& from,
                                bool incoming,
                                const std::time_t& duration = -1);
    /**
     * Add a new message from a peer in the database
     * @param peerId          the author id
     * @param body          the content of the message
     * @param timestamp     the timestamp of the message
     * @param daemonId      the daemon id
     * @return msgId generated (in db)
     */
    QString addIncomingMessage(const QString& peerId,
                               const QString& body,
                               const uint64_t& timestamp = 0,
                               const QString& daemonId = "");
    /**
     * Change the status of an interaction. Listen from callbacksHandler
     * @param accountId, account linked
     * @param messageId, interaction to update
     * @param conversationId, conversation
     * @param peerId, peer id
     * @param status, new status for this interaction
     */
    void updateInteractionStatus(const QString& accountId,
                                 const QString& conversationId,
                                 const QString& peerId,
                                 const QString& messageId,
                                 int status);

    /**
     * place a call
     * @param uid, conversation id
     * @param isAudioOnly, allow to specify if the call is only audio. Set to false by default.
     */
    void placeCall(const QString& uid, bool isAudioOnly = false);

    /**
     * get number of unread messages
     */
    int getNumberOfUnreadMessagesFor(const QString& uid);

    /**
     * Handle data transfer progression
     */
    void updateTransferProgress(QTimer* timer, int conversationIdx, const QString& interactionId);

    bool usefulDataFromDataTransfer(const QString& fileId,
                                    const datatransfer::Info& info,
                                    QString& interactionId,
                                    QString& conversationId);
    void awaitingHost(const QString& fileId, datatransfer::Info info);

    bool hasOneOneSwarmWith(const contact::Info& participant);

    /**
     * accept a file transfer
     * @param convUid
     * @param interactionId
     */
    void acceptTransfer(const QString& convUid, const QString& interactionId);
    void handleIncomingFile(const QString& convId, const QString& interactionId, int totalSize);
    void addConversationRequest(const MapStringString& convRequest, bool emitToClient = false);
    void addContactRequest(const QString& contactUri);

    // filter out ourself from conversation participants.
    const VectorString peersForConversation(const conversation::Info& conversation) const;
    void invalidateModel();
    void emplaceBackConversation(conversation::Info&& conversation);
    void eraseConversation(const QString& convId);
    void eraseConversation(int index);

    const ConversationModel& linked;
    Lrc& lrc;
    Database& db;
    const CallbacksHandler& callbacksHandler;
    const BehaviorController& behaviorController;

    ConversationModel::ConversationQueue conversations; ///< non-filtered conversations
    ConversationModel::ConversationQueue searchResults;

    ConversationModel::ConversationQueueProxy filteredConversations;
    ConversationModel::ConversationQueueProxy customFilteredConversations;

    std::map<QString, int> conversationMap;

    QString currentFilter;
    FilterType typeFilter;
    FilterType customTypeFilter;

    MapStringString transfIdToDbIntId;
    uint32_t mediaResearchRequestId;
    uint32_t msgResearchRequestId;

public Q_SLOTS:
    /**
     * Listen for a peer update from contactModel (new alias, avatar, etc.)
     */
    void slotContactUpdated(const QString& uri);
    /**
     * Listen from contactModel when a new contact is added
     * @param uri
     */
    void slotContactAdded(const QString& contactUri);
    /**
     * Listen from contactModel when a pending contact is accepted
     * @param uri
     */
    void slotPendingContactAccepted(const QString& uri);
    /**
     * Listen from contactModel when aa new contact is removed
     * @param uri
     */
    void slotContactRemoved(const QString& uri);
    /**
     * Listen from callmodel for new calls.
     * @param fromId caller uri
     * @param callId
     * @param isOutgoing
     * @param toUri
     */
    void slotNewCall(const QString& fromId, const QString& callId, bool isOutgoing, const QString& toUri);
    /**
     * Listen from callmodel for calls status changed.
     * @param callId
     */
    void slotCallStatusChanged(const QString& accountId, const QString& callId, int code);
    /**
     * Listen from callmodel for writing "Call started"
     * @param callId
     */
    void slotCallStarted(const QString& callId);
    /**
     * Listen from callmodel for writing "Call ended"
     * @param callId
     */
    void slotCallEnded(const QString& callId);
    /**
     * Listen from CallbacksHandler for new incoming interactions;
     * @param accountId
     * @param msgId
     * @param peerId
     * @param payloads body
     */
    void slotNewAccountMessage(const QString& accountId,
                               const QString& peerId,
                               const QString& msgId,
                               const MapStringString& payloads);
    /**
     * Listen from CallbacksHandler for new messages in a SIP call
     * @param accountId account linked to the interaction
     * @param callId call linked to the interaction
     * @param from author uri
     * @param body of the message
     */
    void slotIncomingCallMessage(const QString& accountId,
                                 const QString& callId,
                                 const QString& from,
                                 const QString& body);
    /**
     * Listen from CallModel when a call is added to a conference
     * @param callId
     * @param conversationId
     * @param confId
     */
    void slotCallAddedToConference(const QString& callId, const QString& conversationId, const QString& confId);
    /**
     * Listen from CallbacksHandler when a conference is deleted.
     * @param accountId
     * @param confId
     */
    void slotConferenceRemoved(const QString& accountId, const QString& confId);
    /**
     * Listen for when a contact is composing
     * @param accountId
     * @param contactUri
     * @param isComposing
     */
    void slotComposingStatusChanged(const QString& accountId,
                                    const QString& convId,
                                    const QString& contactUri,
                                    bool isComposing);

    void slotTransferStatusCreated(const QString& fileId, api::datatransfer::Info info);
    void slotTransferStatusCanceled(const QString& fileId, api::datatransfer::Info info);
    void slotTransferStatusAwaitingPeer(const QString& fileId, api::datatransfer::Info info);
    void slotTransferStatusAwaitingHost(const QString& fileId, api::datatransfer::Info info);
    void slotTransferStatusOngoing(const QString& fileId, api::datatransfer::Info info);
    void slotTransferStatusFinished(const QString& fileId, api::datatransfer::Info info);
    void slotTransferStatusError(const QString& fileId, api::datatransfer::Info info);
    void slotTransferStatusTimeoutExpired(const QString& fileId, api::datatransfer::Info info);
    void slotTransferStatusUnjoinable(const QString& fileId, api::datatransfer::Info info);
    bool updateTransferStatus(const QString& fileId,
                              datatransfer::Info info,
                              interaction::TransferStatus newStatus,
                              bool& updated);
    void slotSwarmLoaded(uint32_t requestId,
                         const QString& accountId,
                         const QString& conversationId,
                         const VectorSwarmMessage& messages);
    /**
     * Listen messageFound signal.
     * Is the search response from MessagesAdapter::getConvMedias()
     * @param requestId token of the request
     * @param accountId
     * @param conversationId
     * @param messages Id of all the messages
     */
    void slotMessagesFound(uint32_t requestId,
                           const QString& accountId,
                           const QString& conversationId,
                           const VectorMapStringString& messages);
    void slotMessageReceived(const QString& accountId, const QString& conversationId, const SwarmMessage& message);
    void slotMessageUpdated(const QString& accountId, const QString& conversationId, const SwarmMessage& message);
    void slotReactionAdded(const QString& accountId,
                           const QString& conversationId,
                           const QString& messageId,
                           const MapStringString& reaction);
    void slotReactionRemoved(const QString& accountId,
                             const QString& conversationId,
                             const QString& messageId,
                             const QString& reactionId);
    void slotConversationProfileUpdated(const QString& accountId,
                                        const QString& conversationId,
                                        const MapStringString& profile);
    void slotConversationRequestReceived(const QString& accountId,
                                         const QString& conversationId,
                                         const MapStringString& metadatas);
    void slotConversationMemberEvent(const QString& accountId,
                                     const QString& conversationId,
                                     const QString& memberUri,
                                     int event);
    void slotOnConversationError(const QString& accountId, const QString& conversationId, int code, const QString& what);
    void slotActiveCallsChanged(const QString& accountId,
                                const QString& conversationId,
                                const VectorMapStringString& activeCalls);
    void slotConversationReady(const QString& accountId, const QString& conversationId);
    void slotConversationRemoved(const QString& accountId, const QString& conversationId);
    void slotConversationPreferencesUpdated(const QString& accountId,
                                            const QString& conversationId,
                                            const MapStringString& preferences);
};

ConversationModel::ConversationModel(const account::Info& owner,
                                     Lrc& lrc,
                                     Database& db,
                                     const CallbacksHandler& callbacksHandler,
                                     const BehaviorController& behaviorController)
    : QObject(nullptr)
    , pimpl_(std::make_unique<ConversationModelPimpl>(*this, lrc, db, callbacksHandler, behaviorController))
    , owner(owner)
{}

void
ConversationModel::initConversations()
{
    pimpl_->initConversations();
}

ConversationModel::~ConversationModel() {}

const ConversationModel::ConversationQueue&
ConversationModel::getConversations() const
{
    return pimpl_->conversations;
}

const ConversationModel::ConversationQueueProxy&
ConversationModel::allFilteredConversations() const
{
    if (!pimpl_->filteredConversations.isDirty())
        return pimpl_->filteredConversations;

    return pimpl_->filteredConversations.filter().sort().validate();
}

QMap<ConferenceableItem, ConferenceableValue>
ConversationModel::getConferenceableConversations(const QString& convId, const QString& filter) const
{
    auto conversationIt = pimpl_->conversationMap.find(convId);
    if (conversationIt == pimpl_->conversationMap.end() || !owner.enabled) {
        return {};
    }
    auto& conversation = pimpl_->conversations.at(conversationIt->second);
    QMap<ConferenceableItem, ConferenceableValue> result;
    ConferenceableValue callsVector, contactsVector;

    auto& currentConfId = conversation.confId;
    auto& currentCallId = conversation.callId;
    auto calls = pimpl_->lrc.getCalls();
    auto conferences = pimpl_->lrc.getConferences(owner.id);
    auto& conversations = pimpl_->conversations;
    auto currentAccountID = pimpl_->linked.owner.id;
    // add contacts for current account
    for (const auto& conv : conversations) {
        // conversations with calls will be added in call section
        // we want to add only contacts non-swarm or one-to-one conversation
        auto& peers = pimpl_->peersForConversation(conv);
        if (!conv.callId.isEmpty() || !conv.confId.isEmpty() || !conv.isCoreDialog() || peers.empty()) {
            continue;
        }
        try {
            auto contact = owner.contactModel->getContact(peers.front());
            if (contact.isBanned || contact.profileInfo.type == profile::Type::PENDING) {
                continue;
            }
            QVector<AccountConversation> cv;
            AccountConversation accConv = {conv.uid, currentAccountID};
            cv.push_back(accConv);
            if (filter.isEmpty()) {
                contactsVector.push_back(cv);
                continue;
            }
            bool result = contact.profileInfo.alias.contains(filter, Qt::CaseInsensitive)
                          || contact.profileInfo.uri.contains(filter, Qt::CaseInsensitive)
                          || contact.registeredName.contains(filter, Qt::CaseInsensitive);
            if (result) {
                contactsVector.push_back(cv);
            }
        } catch (const std::out_of_range& e) {
            qDebug() << e.what();
            continue;
        }
    }

    if (calls.empty() && conferences.empty()) {
        result.insert(ConferenceableItem::CONTACT, contactsVector);
        return result;
    }

    // filter out calls from conference
    for (const auto& c : conferences) {
        for (const auto& subcall : owner.callModel->getConferenceSubcalls(c)) {
            const auto position = std::find(calls.cbegin(), calls.cend(), subcall);
            if (position != calls.cend()) {
                calls.erase(position);
            }
        }
    }

    // found conversations and account for calls and conferences
    QMap<QString, QVector<AccountConversation>> tempConferences;
    for (const auto& account_id : pimpl_->lrc.getAccountModel().getAccountList()) {
        try {
            auto& accountInfo = pimpl_->lrc.getAccountModel().getAccountInfo(account_id);
            auto type = accountInfo.profileInfo.type == profile::Type::SIP ? FilterType::SIP : FilterType::JAMI;
            auto accountConv = accountInfo.conversationModel->getFilteredConversations(type);
            accountConv.for_each([this,
                                  filter,
                                  &accountInfo,
                                  account_id,
                                  currentCallId,
                                  currentConfId,
                                  &conferences,
                                  &calls,
                                  &tempConferences,
                                  &callsVector](const conversation::Info& conv) {
                bool confFilterPredicate = !conv.confId.isEmpty() && conv.confId != currentConfId
                                           && std::find(conferences.begin(), conferences.end(), conv.confId)
                                                  != conferences.end();
                bool callFilterPredicate = !conv.callId.isEmpty() && conv.callId != currentCallId
                                           && std::find(calls.begin(), calls.end(), conv.callId) != calls.end();
                auto& peers = pimpl_->peersForConversation(conv);
                if ((!confFilterPredicate && !callFilterPredicate) || !conv.isCoreDialog()) {
                    return;
                }

                // vector of conversationID accountID pair
                // for call has only one entry, for conference multyple
                QVector<AccountConversation> cv;
                AccountConversation accConv = {conv.uid, account_id};
                cv.push_back(accConv);

                bool isConference = !conv.confId.isEmpty();
                // call could be added if it is not conference and in active state
                bool shouldAddCall = false;
                if (!isConference && accountInfo.callModel->hasCall(conv.callId)) {
                    const auto& call = accountInfo.callModel->getCall(conv.callId);
                    shouldAddCall = call.status == lrc::api::call::Status::PAUSED
                                    || call.status == lrc::api::call::Status::IN_PROGRESS;
                }

                auto contact = accountInfo.contactModel->getContact(peers.front());
                // check if contact satisfy filter
                bool result = (filter.isEmpty() || isConference) ? true
                                                                 : (contact.profileInfo.alias.contains(filter)
                                                                    || contact.profileInfo.uri.contains(filter)
                                                                    || contact.registeredName.contains(filter));
                if (!result) {
                    return;
                }
                if (isConference && tempConferences.count(conv.confId)) {
                    tempConferences.find(conv.confId).value().push_back(accConv);
                } else if (isConference) {
                    tempConferences.insert(conv.confId, cv);
                } else if (shouldAddCall) {
                    callsVector.push_back(cv);
                }
            });
        } catch (...) {
        }
    }
    for (const auto& it : tempConferences.toStdMap()) {
        if (filter.isEmpty()) {
            callsVector.push_back(it.second);
            continue;
        }
        for (const AccountConversation& accConv : it.second) {
            try {
                auto& account = pimpl_->lrc.getAccountModel().getAccountInfo(accConv.accountId);
                auto& conv = account.conversationModel->getConversationForUid(accConv.convId)->get();
                auto& peers = pimpl_->peersForConversation(conv);
                if (!conv.isCoreDialog()) {
                    continue;
                }
                auto cont = account.contactModel->getContact(peers.front());
                if (cont.profileInfo.alias.contains(filter) || cont.profileInfo.uri.contains(filter)
                    || cont.registeredName.contains(filter)) {
                    callsVector.push_back(it.second);
                    continue;
                }
            } catch (...) {
            }
        }
    }
    result.insert(ConferenceableItem::CALL, callsVector);
    result.insert(ConferenceableItem::CONTACT, contactsVector);
    return result;
}

const ConversationModel::ConversationQueue&
ConversationModel::getAllSearchResults() const
{
    return pimpl_->searchResults;
}

const ConversationModel::ConversationQueueProxy&
ConversationModel::getFilteredConversations(const FilterType& filter, bool forceUpdate, const bool includeBanned) const
{
    if (pimpl_->customTypeFilter == filter && !pimpl_->customFilteredConversations.isDirty() && !forceUpdate)
        return pimpl_->customFilteredConversations;

    pimpl_->customTypeFilter = filter;
    return pimpl_->customFilteredConversations.reset(pimpl_->conversations)
        .filter([this, &includeBanned](const conversation::Info& entry) {
            try {
                if (entry.isLegacy()) {
                    auto& peers = pimpl_->peersForConversation(entry);
                    if (peers.isEmpty()) {
                        return false;
                    }
                    auto contactInfo = owner.contactModel->getContact(peers.front());
                    // do not check blocked contacts for conversation with many participants
                    if (!includeBanned && (contactInfo.isBanned && peers.size() == 1))
                        return false;
                }
                switch (pimpl_->customTypeFilter) {
                case FilterType::JAMI:
                    // we have conversation with many participants only for JAMI
                    return (owner.profileInfo.type == profile::Type::JAMI && !entry.isRequest);
                case FilterType::SIP:
                    return (owner.profileInfo.type == profile::Type::SIP && !entry.isRequest);
                case FilterType::REQUEST:
                    return entry.isRequest;
                case FilterType::INVALID:
                default:
                    break;
                }
            } catch (...) {
            }
            return false;
        })
        .validate();
}

const ConversationModel::ConversationQueueProxy&
ConversationModel::getFilteredConversations(const profile::Type& profileType,
                                            bool forceUpdate,
                                            const bool includeBanned) const
{
    FilterType filterType = FilterType::INVALID;
    switch (profileType) {
    case lrc::api::profile::Type::JAMI:
        filterType = lrc::api::FilterType::JAMI;
        break;
    case lrc::api::profile::Type::SIP:
        filterType = lrc::api::FilterType::SIP;
        break;
    default:
        break;
    }

    return getFilteredConversations(filterType, forceUpdate, includeBanned);
}

OptRef<conversation::Info>
ConversationModel::getConversationForUid(const QString& uid) const
{
    if (!pimpl_) {
        qWarning() << "Invalid pimpl_";
        return std::nullopt;
    }
    try {
        return std::make_optional(pimpl_->getConversationForUid(uid, true));
    } catch (const std::out_of_range&) {
        return std::nullopt;
    }
}

OptRef<conversation::Info>
ConversationModel::getConversationForPeerUri(const QString& uri) const
{
    try {
        return std::make_optional(pimpl_->getConversation(
            [this, uri](const conversation::Info& conv) -> bool {
                if (!conv.isCoreDialog()) {
                    return false;
                }
                if (conv.mode == conversation::Mode::ONE_TO_ONE) {
                    return pimpl_->peersForConversation(conv).indexOf(uri) != -1;
                }
                return uri == pimpl_->peersForConversation(conv).front();
            },
            true));
    } catch (const std::out_of_range&) {
        return std::nullopt;
    }
}

OptRef<conversation::Info>
ConversationModel::getConversationForCallId(const QString& callId) const
{
    try {
        return std::make_optional(
            pimpl_->getConversation([callId](const conversation::Info& conv)
                                        -> bool { return (callId == conv.callId || callId == conv.confId); },
                                    true));
    } catch (const std::out_of_range&) {
        return std::nullopt;
    }
}

OptRef<conversation::Info>
ConversationModel::filteredConversation(unsigned row) const
{
    auto conversations = allFilteredConversations();
    if (row >= conversations.get().size())
        return std::nullopt;

    return std::make_optional(conversations.get().at(row));
}

OptRef<conversation::Info>
ConversationModel::searchResultForRow(unsigned row) const
{
    auto& results = pimpl_->searchResults;
    if (row >= results.size())
        return std::nullopt;

    return std::make_optional(std::ref(results.at(row)));
}

void
ConversationModel::makePermanent(const QString& uid)
{
    try {
        auto& conversation = pimpl_->getConversationForUid(uid, true).get();

        if (conversation.participants.empty()) {
            // Should not
            qDebug() << "ConversationModel::addConversation cannot add a conversation with no "
                        "participant.";
            return;
        }

        // Send contact request if non used
        auto& peers = pimpl_->peersForConversation(conversation);
        if (peers.size() != 1) {
            return;
        }
        pimpl_->sendContactRequest(peers.front());
    } catch (const std::out_of_range& e) {
        qDebug() << "Make permanent failed. Conversation not found.";
    }
}

void
ConversationModel::selectConversation(const QString& uid) const
{
    try {
        auto& conversation = pimpl_->getConversationForUid(uid, true).get();

        bool callEnded = true;
        if (!conversation.callId.isEmpty()) {
            try {
                auto call = owner.callModel->getCall(conversation.callId);
                callEnded = call.status == call::Status::ENDED;
            } catch (...) {
            }
        }
        if (!conversation.confId.isEmpty() && owner.confProperties.isRendezVous) {
            // If we are on a rendez vous account and we select the conversation,
            // attach to the call.
            CallManager::instance().unholdConference(owner.id, conversation.confId);
        }

        if (not callEnded and not conversation.confId.isEmpty()) {
            Q_EMIT pimpl_->behaviorController.showCallView(owner.id, conversation.uid);
        } else if (callEnded) {
            Q_EMIT pimpl_->behaviorController.showChatView(owner.id, conversation.uid);
        } else {
            try {
                auto call = owner.callModel->getCall(conversation.callId);
                switch (call.status) {
                case call::Status::INCOMING_RINGING:
                case call::Status::OUTGOING_RINGING:
                case call::Status::CONNECTING:
                case call::Status::SEARCHING:
                    // We are currently in a call
                    Q_EMIT pimpl_->behaviorController.showIncomingCallView(owner.id, conversation.uid);
                    break;
                case call::Status::PAUSED:
                case call::Status::CONNECTED:
                case call::Status::IN_PROGRESS:
                    // We are currently receiving a call
                    Q_EMIT pimpl_->behaviorController.showCallView(owner.id, conversation.uid);
                    break;
                case call::Status::PEER_BUSY:
                    Q_EMIT pimpl_->behaviorController.showLeaveMessageView(owner.id, conversation.uid);
                    break;
                case call::Status::TIMEOUT:
                case call::Status::TERMINATING:
                case call::Status::INVALID:
                case call::Status::INACTIVE:
                    // call just ended
                    Q_EMIT pimpl_->behaviorController.showChatView(owner.id, conversation.uid);
                    break;
                case call::Status::ENDED:
                default: // ENDED
                    // nothing to do
                    break;
                }
            } catch (const std::out_of_range&) {
                // Should not happen
                Q_EMIT pimpl_->behaviorController.showChatView(owner.id, conversation.uid);
            }
        }
    } catch (const std::out_of_range& e) {
        qDebug() << "Select conversation failed. Conversation does not exist.";
    }
}

void
ConversationModel::removeConversation(const QString& uid, bool banned, bool keepContact)
{
    // Get conversation
    auto conversationIdx = pimpl_->indexOf(uid);
    if (conversationIdx == -1)
        return;

    auto& conversation = pimpl_->conversations.at(conversationIdx);
    // Remove contact from daemon
    // NOTE: this will also remove the conversation into the database for non-swarm and remove
    // conversation repository for one-to-one.
    auto& peers = pimpl_->peersForConversation(conversation);
    if (peers.empty()) {
        // Should not
        qDebug() << "ConversationModel::removeConversation cannot remove a conversation without "
                    "participant.";
        return;
    }
    if (conversation.isSwarm() && !banned && (!conversation.isCoreDialog() || keepContact)) {
        if (conversation.isRequest) {
            ConfigurationManager::instance().declineConversationRequest(owner.id, uid);
        } else {
            ConfigurationManager::instance().removeConversation(owner.id, uid);
        }
    } else {
        try {
            auto& contact = owner.contactModel->getContact(peers.front());
            owner.contactModel->removeContact(peers.front(), banned);
        } catch (const std::out_of_range&) {
            qWarning() << "Contact not found: " << peers.front();
            ConfigurationManager::instance().removeConversation(owner.id, uid);
        }
    }
}

void
ConversationModel::deleteObsoleteHistory(int days)
{
    if (days < 1)
        return; // unlimited history

    auto currentTime = static_cast<long int>(std::time(nullptr)); // since epoch, in seconds...
    auto date = currentTime - (days * 86400);

    storage::deleteObsoleteHistory(pimpl_->db, date);
}

void
ConversationModel::joinCall(
    const QString& uid, const QString& uri, const QString& deviceId, const QString& confId, bool isAudioOnly)
{
    try {
        auto& conversation = pimpl_->getConversationForUid(uid, true).get();
        if (!conversation.callId.isEmpty()) {
            qWarning() << "Already in a call for swarm:" + uid;
            return;
        }
        conversation.callId = owner.callModel->createCall("rdv:" + uid + "/" + uri + "/" + deviceId + "/" + confId,
                                                          isAudioOnly);
        // Update interaction status
        pimpl_->invalidateModel();
        selectConversation(uid);
        Q_EMIT conversationUpdated(uid);
    } catch (...) {
    }
}

void
ConversationModelPimpl::placeCall(const QString& uid, bool isAudioOnly)
{
    try {
        auto& conversation = getConversationForUid(uid, true).get();
        if (conversation.participants.empty()) {
            // Should not
            qDebug() << "ConversationModel::placeCall cannot call a conversation without participants.";
            return;
        }

        if (!conversation.isCoreDialog() && conversation.isSwarm()) {
            qDebug() << "Start call for swarm:" + uid;
            conversation.callId = linked.owner.callModel->createCall("swarm:" + uid, isAudioOnly);

            // Update interaction status
            invalidateModel();
            linked.selectConversation(conversation.uid);
            Q_EMIT linked.conversationUpdated(conversation.uid);
            Q_EMIT linked.dataChanged(indexOf(conversation.uid));
            return;
        }

        auto& peers = peersForConversation(conversation);
        // there is no calls in group with more than 2 participants
        if (peers.size() != 1) {
            return;
        }
        // Disallow multiple call
        if (!conversation.callId.isEmpty()) {
            try {
                auto call = linked.owner.callModel->getCall(conversation.callId);
                switch (call.status) {
                case call::Status::INCOMING_RINGING:
                case call::Status::OUTGOING_RINGING:
                case call::Status::CONNECTING:
                case call::Status::SEARCHING:
                case call::Status::PAUSED:
                case call::Status::IN_PROGRESS:
                case call::Status::CONNECTED:
                    return;
                case call::Status::INVALID:
                case call::Status::INACTIVE:
                case call::Status::ENDED:
                case call::Status::PEER_BUSY:
                case call::Status::TIMEOUT:
                case call::Status::TERMINATING:
                default:
                    break;
                }
            } catch (const std::out_of_range&) {
            }
        }

        auto convId = uid;

        auto participant = peers.front();
        bool isTemporary = convId == participant || convId == "SEARCHSIP";
        auto contactInfo = linked.owner.contactModel->getContact(participant);
        auto uri = contactInfo.profileInfo.uri;

        if (uri.isEmpty())
            return; // Incorrect item

        // Don't call banned contact
        if (contactInfo.isBanned) {
            qDebug() << "ContactModel::placeCall: denied, contact is banned.";
            return;
        }

        if (linked.owner.profileInfo.type != profile::Type::SIP) {
            uri = "ring:" + uri; // Add the ring: before or it will fail.
        }

        auto cb = ([this, isTemporary, uri, isAudioOnly, &conversation](QString conversationId) {
            if (indexOf(conversationId) < 0) {
                qDebug() << "Cannot place call: conversation does not exist.";
                return;
            }

            auto& newConv = isTemporary ? getConversationForUid(conversationId).get() : conversation;

            newConv.callId = linked.owner.callModel->createCall(uri, isAudioOnly);
            if (newConv.callId.isEmpty()) {
                qDebug() << "Cannot place call (daemon side failure?)";
                return;
            }

            invalidateModel();

            Q_EMIT behaviorController.showIncomingCallView(linked.owner.id, newConv.uid);
        });

        if (isTemporary) {
            QMetaObject::Connection* const connection = new QMetaObject::Connection;
            *connection = connect(&this->linked,
                                  &ConversationModel::conversationReady,
                                  [cb, connection, convId](QString conversationId, QString participantId) {
                                      if (participantId != convId && convId != "SEARCHSIP") {
                                          return;
                                      }
                                      cb(conversationId);
                                      QObject::disconnect(*connection);
                                      if (connection) {
                                          delete connection;
                                      }
                                  });
        }

        sendContactRequest(participant);

        if (!isTemporary) {
            cb(convId);
        }
    } catch (const std::out_of_range& e) {
        qDebug() << "Cannot place call as conversation does not exist.";
    }
}

void
ConversationModel::placeAudioOnlyCall(const QString& uid)
{
    pimpl_->placeCall(uid, true);
}

void
ConversationModel::placeCall(const QString& uid)
{
    pimpl_->placeCall(uid);
}

MapStringString
ConversationModel::getConversationInfos(const QString& conversationId)
{
    MapStringString ret = ConfigurationManager::instance().conversationInfos(owner.id, conversationId);
    return ret;
}

MapStringString
ConversationModel::getConversationPreferences(const QString& conversationId)
{
    MapStringString ret = ConfigurationManager::instance().getConversationPreferences(owner.id, conversationId);
    return ret;
}

QString
ConversationModel::createConversation(const VectorString& participants, const MapStringString& infos)
{
    auto convUid = ConfigurationManager::instance().startConversation(owner.id);
    pimpl_->addSwarmConversation(convUid);
    if (!infos.isEmpty())
        updateConversationInfos(convUid, infos);
    for (const auto& participant : participants)
        ConfigurationManager::instance().addConversationMember(owner.id, convUid, participant);
    Q_EMIT newConversation(convUid);
    pimpl_->invalidateModel();
    Q_EMIT modelChanged();
    return convUid;
}

void
ConversationModel::updateConversationInfos(const QString& conversationId, const MapStringString infos)
{
    auto conversationOpt = getConversationForUid(conversationId);
    if (!conversationOpt.has_value())
        return;
    auto& conversation = conversationOpt->get();
    if (conversation.isCoreDialog()) {
        // If 1:1, we override a profile (as the peer will send their new profiles)
        auto peer = pimpl_->peersForConversation(conversation);
        if (!peer.isEmpty())
            owner.contactModel->updateContact(peer.at(0), infos);
        return;
    }
    MapStringString newInfos = infos;
    // Compress avatar as it will be sent in the conversation's request over the DHT
    if (infos.contains("avatar"))
        newInfos["avatar"] = storage::vcard::compressedAvatar(infos["avatar"]);
    ConfigurationManager::instance().updateConversationInfos(owner.id, conversationId, newInfos);
}

void
ConversationModel::popFrontError(const QString& conversationId)
{
    auto conversationOpt = getConversationForUid(conversationId);
    if (!conversationOpt.has_value())
        return;

    auto& conversation = conversationOpt->get();
    conversation.errors.pop_front();
    Q_EMIT conversationErrorsUpdated(conversationId);
}

void
ConversationModel::ignoreActiveCall(const QString& conversationId,
                                    const QString& id,
                                    const QString& uri,
                                    const QString& device)
{
    auto conversationOpt = getConversationForUid(conversationId);
    if (!conversationOpt.has_value())
        return;

    auto& conversation = conversationOpt->get();
    MapStringString mapCall;
    mapCall["id"] = id;
    mapCall["uri"] = uri;
    mapCall["device"] = device;
    conversation.ignoredActiveCalls.push_back(mapCall);
    Q_EMIT activeCallsChanged(owner.id, conversationId);
}

void
ConversationModel::setConversationPreferences(const QString& conversationId, const MapStringString prefs)
{
    ConfigurationManager::instance().setConversationPreferences(owner.id, conversationId, prefs);
}

bool
ConversationModel::hasPendingRequests() const
{
    return pendingRequestCount() > 0;
}

int
ConversationModel::pendingRequestCount() const
{
    int pendingRequestCount = 0;
    std::for_each(pimpl_->conversations.begin(), pimpl_->conversations.end(), [&pendingRequestCount](const auto& c) {
        pendingRequestCount += c.isRequest;
    });
    return pendingRequestCount;
}

int
ConversationModel::notificationsCount() const
{
    int notificationsCount = 0;
    std::for_each(pimpl_->conversations.begin(), pimpl_->conversations.end(), [&notificationsCount](const auto& c) {
        if (c.preferences["ignoreNotifications"] == "true") {
            return;
        }
        if (c.isRequest)
            notificationsCount += 1;
        else {
            notificationsCount += c.unreadMessages;
        }
    });
    return notificationsCount;
}

void
ConversationModel::reloadHistory() const
{
    std::for_each(pimpl_->conversations.begin(), pimpl_->conversations.end(), [&](const conversation::Info& c) {
        c.interactions->reloadHistory();
        Q_EMIT conversationUpdated(c.uid);
        Q_EMIT dataChanged(pimpl_->indexOf(c.uid));
    });
}

QString
ConversationModel::title(const QString& conversationId) const
{
    auto conversationOpt = getConversationForUid(conversationId);
    if (!conversationOpt.has_value()) {
        return {};
    }
    auto& conversation = conversationOpt->get();
    if (conversation.isCoreDialog()) {
        auto peer = pimpl_->peersForConversation(conversation);
        if (peer.isEmpty())
            return {};
        // In this case, we can just display contact name
        if (peer.at(0) == owner.profileInfo.uri)
            return QObject::tr("%1 (you)").arg(owner.accountModel->bestNameForAccount(owner.id));
        return owner.contactModel->bestNameForContact(peer.at(0));
    }
    if (conversation.infos["title"] != "") {
        return conversation.infos["title"];
    }
    // NOTE: Do not call any daemon method there as title() is called a lot for drawing
    QString title;
    auto idx = 0u;
    auto others = 0;
    for (const auto& member : conversation.participants) {
        QString name;
        if (member.uri == owner.profileInfo.uri) {
            name = QObject::tr("%1 (you)").arg(owner.accountModel->bestNameForAccount(owner.id));
        } else {
            name = owner.contactModel->bestNameForContact(member.uri);
        }
        if (title.length() + name.length() > 32) {
            // Avoid too long titles
            others += 1;
            continue;
        }
        title += name;
        idx += 1;
        if (idx != conversation.participants.size() || others != 0) {
            title += ", ";
        }
    }
    if (others != 0) {
        title += QString("+ %1").arg(others);
    }
    return title;
}

member::Role
ConversationModel::memberRole(const QString& conversationId, const QString& memberUri) const
{
    auto conversationOpt = getConversationForUid(conversationId);
    if (!conversationOpt.has_value())
        throw std::out_of_range("Member out of range");
    auto& conversation = conversationOpt->get();
    for (const auto& p : conversation.participants) {
        if (p.uri == memberUri)
            return p.role;
    }
    throw std::out_of_range("Member out of range");
}

QString
ConversationModel::description(const QString& conversationId) const
{
    auto conversationOpt = getConversationForUid(conversationId);
    if (!conversationOpt.has_value())
        return {};
    auto& conversation = conversationOpt->get();
    if (conversation.isCoreDialog()) {
        auto peer = pimpl_->peersForConversation(conversation);
        if (peer.isEmpty())
            return {};
        return owner.contactModel->bestIdForContact(peer.front());
    }
    return conversation.infos["description"];
}

QString
ConversationModel::avatar(const QString& conversationId) const
{
    auto conversationOpt = getConversationForUid(conversationId);
    if (!conversationOpt.has_value()) {
        return {};
    }
    auto& conversation = conversationOpt->get();
    if (conversation.isCoreDialog()) {
        auto peer = pimpl_->peersForConversation(conversation);
        if (peer.isEmpty())
            return {};
        // In this case, we can just display contact name
        return owner.contactModel->avatar(peer.at(0));
    }
    // We need to strip the whitespace characters for the avatar
    // when it comes from the conversation info.
    return conversation.infos["avatar"].simplified();
}

void
ConversationModel::sendMessage(const QString& uid, const QString& body, const QString& parentId)
{
    try {
        auto& conversation = pimpl_->getConversationForUid(uid, true).get();
        if (!conversation.isLegacy()) {
            ConfigurationManager::instance().sendMessage(owner.id,
                                                         uid,
                                                         body,
                                                         parentId,
                                                         static_cast<int>(MessageFlag::Text));
            return;
        }

        auto& peers = pimpl_->peersForConversation(conversation);
        if (peers.isEmpty()) {
            // Should not
            qDebug() << "ConversationModel::sendMessage cannot send an interaction to a conversation "
                        "with no participants.";
            return;
        }
        auto convId = uid;
        auto& peerId = peers.front();
        bool isTemporary = peerId == convId || convId == "SEARCHSIP";

        auto cb = ([this, isTemporary, body, &conversation, parentId, convId](QString conversationId) {
            if (pimpl_->indexOf(conversationId) < 0) {
                return;
            }
            auto& newConv = isTemporary ? pimpl_->getConversationForUid(conversationId).get() : conversation;

            if (newConv.isSwarm()) {
                ConfigurationManager::instance().sendMessage(owner.id,
                                                             conversationId,
                                                             body,
                                                             parentId,
                                                             static_cast<int>(MessageFlag::Text));
                return;
            }
            auto& peers = pimpl_->peersForConversation(newConv);
            if (peers.isEmpty()) {
                return;
            }

            uint64_t daemonMsgId = 0;
            auto status = interaction::Status::SENDING;
            auto convId = newConv.uid;

            QStringList callLists = CallManager::instance().getCallList(""); // no auto
            // workaround: sometimes, it may happen that the daemon delete a call, but lrc
            // don't. We check if the call is
            //             still valid every time the user want to send a message.
            if (not newConv.callId.isEmpty() and not callLists.contains(newConv.callId))
                newConv.callId.clear();

            if (not newConv.callId.isEmpty() and call::canSendSIPMessage(owner.callModel->getCall(newConv.callId))) {
                status = interaction::Status::UNKNOWN;
                owner.callModel->sendSipMessage(newConv.callId, body);

            } else {
                daemonMsgId = owner.contactModel->sendDhtMessage(peers.front(), body);
            }

            // Add interaction to database
            interaction::Info
                msg {owner.profileInfo.uri, body, std::time(nullptr), 0, interaction::Type::TEXT, status, true};
            auto msgId = storage::addMessageToConversation(pimpl_->db, convId, msg);

            // Update conversation
            if (status == interaction::Status::SENDING) {
                // Because the daemon already give an id for the message, we need to store it.
                storage::addDaemonMsgId(pimpl_->db, msgId, toQString(daemonMsgId));
            }

            if (!newConv.interactions->append(msgId, msg)) {
                qWarning() << Q_FUNC_INFO << "Append failed: duplicate ID";
                return;
            }

            newConv.lastSelfMessageId = msgId;
            // Emit this signal for chatview in the client
            Q_EMIT newInteraction(convId, msgId, msg);
            // This conversation is now at the top of the list
            // The order has changed, informs the client to redraw the list
            pimpl_->invalidateModel();
            Q_EMIT modelChanged();
            Q_EMIT dataChanged(pimpl_->indexOf(convId));
        });

        if (isTemporary) {
            QMetaObject::Connection* const connection = new QMetaObject::Connection;
            *connection = connect(this,
                                  &ConversationModel::conversationReady,
                                  [cb, connection, convId](QString conversationId, QString participantId) {
                                      if (participantId != convId && convId != "SEARCHSIP") {
                                          return;
                                      }
                                      cb(conversationId);
                                      QObject::disconnect(*connection);
                                      if (connection) {
                                          delete connection;
                                      }
                                  });
        }
        pimpl_->sendContactRequest(peerId);
        if (!isTemporary) {
            cb(convId);
        }
    } catch (const std::out_of_range& e) {
        qDebug() << "Cannot send message as conversation does not exist.";
    }
}

void
ConversationModel::editMessage(const QString& convId, const QString& newBody, const QString& messageId)
{
    auto conversationOpt = getConversationForUid(convId);
    if (!conversationOpt.has_value()) {
        return;
    }
    ConfigurationManager::instance().sendMessage(owner.id,
                                                 convId,
                                                 newBody,
                                                 messageId,
                                                 static_cast<int>(MessageFlag::Reply));
}

void
ConversationModel::reactMessage(const QString& convId, const QString& emoji, const QString& messageId)
{
    auto conversationOpt = getConversationForUid(convId);
    if (!conversationOpt.has_value()) {
        return;
    }
    ConfigurationManager::instance().sendMessage(owner.id,
                                                 convId,
                                                 emoji,
                                                 messageId,
                                                 static_cast<int>(MessageFlag::Reaction));
}

void
ConversationModel::refreshFilter()
{
    pimpl_->invalidateModel();
    Q_EMIT filterChanged();
}

void
ConversationModel::updateSearchStatus(const QString& status) const
{
    Q_EMIT searchStatusChanged(status);
}

void
ConversationModel::setFilter(const QString& filter)
{
    pimpl_->currentFilter = filter;
    pimpl_->invalidateModel();
    pimpl_->searchResults.clear();
    Q_EMIT searchResultUpdated();
    owner.contactModel->searchContact(filter);
    Q_EMIT filterChanged();
}

void
ConversationModel::setFilter(const FilterType& filter)
{
    // Switch between PENDING, RING and SIP contacts.
    pimpl_->typeFilter = filter;
    pimpl_->invalidateModel();
    Q_EMIT filterChanged();
}

void
ConversationModel::joinConversations(const QString& uidA, const QString& uidB)
{
    auto conversationAIdx = pimpl_->indexOf(uidA);
    auto conversationBIdx = pimpl_->indexOf(uidB);
    if (conversationAIdx == -1 || conversationBIdx == -1 || !owner.enabled)
        return;
    auto& conversationA = pimpl_->conversations[conversationAIdx];
    auto& conversationB = pimpl_->conversations[conversationBIdx];

    if (conversationA.callId.isEmpty() || conversationB.callId.isEmpty())
        return;

    if (conversationA.confId.isEmpty()) {
        if (conversationB.confId.isEmpty()) {
            owner.callModel->joinCalls(conversationA.callId, conversationB.callId);
        } else {
            owner.callModel->joinCalls(conversationA.callId, conversationB.confId);
            conversationA.confId = conversationB.confId;
        }
    } else {
        if (conversationB.confId.isEmpty()) {
            owner.callModel->joinCalls(conversationA.confId, conversationB.callId);
            conversationB.confId = conversationA.confId;
        } else {
            owner.callModel->joinCalls(conversationA.confId, conversationB.confId);
            conversationB.confId = conversationA.confId;
        }
    }
}

void
ConversationModel::clearHistory(const QString& uid)
{
    auto conversationIdx = pimpl_->indexOf(uid);
    if (conversationIdx == -1)
        return;

    auto& conversation = pimpl_->conversations.at(conversationIdx);
    // Remove all TEXT interactions from database
    storage::clearHistory(pimpl_->db, uid);
    // Update conversation
    conversation.interactions->clear();
    storage::getHistory(pimpl_->db,
                        conversation,
                        pimpl_->linked.owner.profileInfo.uri); // will contain "Conversation started"

    Q_EMIT modelChanged();
    Q_EMIT conversationCleared(uid);
    Q_EMIT dataChanged(conversationIdx);
}

bool
ConversationModel::isLastDisplayed(const QString& convId, const QString& interactionId, const QString participant)
{
    auto conversationIdx = pimpl_->indexOf(convId);
    try {
        auto& conversation = pimpl_->conversations.at(conversationIdx);
        return conversation.interactions->getRead(participant) == interactionId;
    } catch (const std::out_of_range& e) {
    }
    return false;
}

void
ConversationModel::clearAllHistory()
{
    storage::clearAllHistory(pimpl_->db);

    for (auto& conversation : pimpl_->conversations) {
        {
            if (conversation.isSwarm()) {
                // WARNING: clear all history is not implemented for swarm
                continue;
            }
            conversation.interactions->clear();
        }
        storage::getHistory(pimpl_->db, conversation, pimpl_->linked.owner.profileInfo.uri);
        Q_EMIT dataChanged(pimpl_->indexOf(conversation.uid));
    }
    Q_EMIT modelChanged();
}

void
ConversationModel::clearUnreadInteractions(const QString& convId)
{
    auto conversationOpt = getConversationForUid(convId);
    if (!conversationOpt.has_value()) {
        return;
    }
    auto& conversation = conversationOpt->get();
    bool updated = false;
    QString lastDisplayedId;
    if (conversation.isSwarm()) {
        updated = true;
        conversation.interactions->withLast([&](const QString& id, interaction::Info&) { lastDisplayedId = id; });
    } else {
        conversation.interactions->forEach([&](const QString& id, interaction::Info& interaction) {
            if (interaction.isRead)
                return;
            updated = true;
            interaction.isRead = true;
            if (owner.profileInfo.type != profile::Type::SIP)
                lastDisplayedId = storage::getDaemonIdByInteractionId(pimpl_->db, id);
            storage::setInteractionRead(pimpl_->db, id);
        });
    }
    if (!lastDisplayedId.isEmpty()) {
        auto to = conversation.isSwarm() ? "swarm:" + convId
                                         : "jami:" + pimpl_->peersForConversation(conversation).front();
        ConfigurationManager::instance().setMessageDisplayed(owner.id, to, lastDisplayedId, 3);
    }
    if (updated) {
        conversation.unreadMessages = 0;
        pimpl_->invalidateModel();
        Q_EMIT conversationUpdated(convId);
        Q_EMIT dataChanged(pimpl_->indexOf(convId));
    }
}

int
ConversationModel::loadConversationMessages(const QString& conversationId, const int size)
{
    auto conversationOpt = getConversationForUid(conversationId);
    if (!conversationOpt.has_value()) {
        return -1;
    }
    auto& conversation = conversationOpt->get();
    if (conversation.allMessagesLoaded) {
        return -1;
    }
    QString lastMsgId;
    conversation.interactions->withLast([&lastMsgId](const QString& id, interaction::Info&) { lastMsgId = id; });
    return ConfigurationManager::instance().loadConversation(owner.id, conversationId, lastMsgId, size);
}

void
ConversationModel::acceptConversationRequest(const QString& conversationId)
{
    auto conversationOpt = getConversationForUid(conversationId);
    if (!conversationOpt.has_value())
        return;
    auto& conversation = conversationOpt->get();
    auto& peers = pimpl_->peersForConversation(conversation);
    if (peers.isEmpty())
        return;

    if (conversation.isSwarm()) {
        conversation.needsSyncing = true;
        Q_EMIT conversationUpdated(conversation.uid);
        pimpl_->invalidateModel();
        Q_EMIT modelChanged();
        ConfigurationManager::instance().acceptConversationRequest(owner.id, conversationId);
    } else {
        pimpl_->sendContactRequest(peers.front());
        try {
            auto contact = owner.contactModel->getContact(peers.front());
            auto notAdded = contact.profileInfo.type == profile::Type::TEMPORARY
                            || contact.profileInfo.type == profile::Type::PENDING;
            if (notAdded) {
                contact.profileInfo.type = profile::Type::TEMPORARY;
                owner.contactModel->addContact(contact);
                return;
            }
        } catch (std::out_of_range& e) {
            qWarning() << e.what();
        }
    }
}

const VectorString
ConversationModel::peersForConversation(const QString& conversationId)
{
    const auto conversationOpt = getConversationForUid(conversationId);
    if (!conversationOpt.has_value()) {
        return {};
    }
    const auto& conversation = conversationOpt->get();
    return pimpl_->peersForConversation(conversation);
}

void
ConversationModel::addConversationMember(const QString& conversationId, const QString& memberId)
{
    ConfigurationManager::instance().addConversationMember(owner.id, conversationId, memberId);
}

void
ConversationModel::removeConversationMember(const QString& conversationId, const QString& memberId)
{
    ConfigurationManager::instance().removeConversationMember(owner.id, conversationId, memberId);
}

ConversationModelPimpl::ConversationModelPimpl(const ConversationModel& linked,
                                               Lrc& lrc,
                                               Database& db,
                                               const CallbacksHandler& callbacksHandler,
                                               const BehaviorController& behaviorController)
    : linked(linked)
    , lrc {lrc}
    , db(db)
    , callbacksHandler(callbacksHandler)
    , typeFilter(FilterType::INVALID)
    , customTypeFilter(FilterType::INVALID)
    , behaviorController(behaviorController)
{
    filteredConversations.bindSortCallback(this, &ConversationModelPimpl::sort);
    filteredConversations.bindFilterCallback(this, &ConversationModelPimpl::filter);

    initConversations();

    // Contact related
    connect(&*linked.owner.contactModel,
            &ContactModel::contactUpdated,
            this,
            &ConversationModelPimpl::slotContactUpdated);
    connect(&*linked.owner.contactModel, &ContactModel::contactAdded, this, &ConversationModelPimpl::slotContactAdded);
    connect(&*linked.owner.contactModel,
            &ContactModel::pendingContactAccepted,
            this,
            &ConversationModelPimpl::slotPendingContactAccepted);
    connect(&*linked.owner.contactModel,
            &ContactModel::contactRemoved,
            this,
            &ConversationModelPimpl::slotContactRemoved);

    // Messages related
    connect(&*linked.owner.contactModel,
            &lrc::ContactModel::newAccountMessage,
            this,
            &ConversationModelPimpl::slotNewAccountMessage);
    connect(&callbacksHandler,
            &CallbacksHandler::incomingCallMessage,
            this,
            &ConversationModelPimpl::slotIncomingCallMessage);
    connect(&callbacksHandler,
            &CallbacksHandler::accountMessageStatusChanged,
            this,
            &ConversationModelPimpl::updateInteractionStatus);

    // Call related
    connect(&*linked.owner.contactModel, &ContactModel::newCall, this, &ConversationModelPimpl::slotNewCall);
    connect(&*linked.owner.callModel,
            &lrc::api::CallModel::callStatusChanged,
            this,
            &ConversationModelPimpl::slotCallStatusChanged);
    connect(&*linked.owner.callModel, &lrc::api::CallModel::callStarted, this, &ConversationModelPimpl::slotCallStarted);
    connect(&*linked.owner.callModel, &lrc::api::CallModel::callEnded, this, &ConversationModelPimpl::slotCallEnded);
    connect(&*linked.owner.callModel,
            &lrc::api::CallModel::callAddedToConference,
            this,
            &ConversationModelPimpl::slotCallAddedToConference);
    connect(&callbacksHandler,
            &CallbacksHandler::conferenceRemoved,
            this,
            &ConversationModelPimpl::slotConferenceRemoved);
    connect(&ConfigurationManager::instance(),
            &ConfigurationManagerInterface::composingStatusChanged,
            this,
            &ConversationModelPimpl::slotComposingStatusChanged);
    connect(&callbacksHandler, &CallbacksHandler::needsHost, this, [&](auto accountId, auto convId) {
        if (accountId != linked.owner.id)
            return;
        emit linked.needsHost(convId);
    });

    // data transfer
    connect(&*linked.owner.contactModel,
            &ContactModel::newAccountTransfer,
            this,
            &ConversationModelPimpl::slotTransferStatusCreated);
    connect(&callbacksHandler,
            &CallbacksHandler::transferStatusCanceled,
            this,
            &ConversationModelPimpl::slotTransferStatusCanceled);
    connect(&callbacksHandler,
            &CallbacksHandler::transferStatusAwaitingPeer,
            this,
            &ConversationModelPimpl::slotTransferStatusAwaitingPeer);
    connect(&callbacksHandler,
            &CallbacksHandler::transferStatusAwaitingHost,
            this,
            &ConversationModelPimpl::slotTransferStatusAwaitingHost);
    connect(&callbacksHandler,
            &CallbacksHandler::transferStatusOngoing,
            this,
            &ConversationModelPimpl::slotTransferStatusOngoing);
    connect(&callbacksHandler,
            &CallbacksHandler::transferStatusFinished,
            this,
            &ConversationModelPimpl::slotTransferStatusFinished);
    connect(&callbacksHandler,
            &CallbacksHandler::transferStatusError,
            this,
            &ConversationModelPimpl::slotTransferStatusError);
    connect(&callbacksHandler,
            &CallbacksHandler::transferStatusTimeoutExpired,
            this,
            &ConversationModelPimpl::slotTransferStatusTimeoutExpired);
    connect(&callbacksHandler,
            &CallbacksHandler::transferStatusUnjoinable,
            this,
            &ConversationModelPimpl::slotTransferStatusUnjoinable);
    // swarm conversations
    connect(&callbacksHandler, &CallbacksHandler::swarmLoaded, this, &ConversationModelPimpl::slotSwarmLoaded);
    connect(&callbacksHandler, &CallbacksHandler::messagesFound, this, &ConversationModelPimpl::slotMessagesFound);
    connect(&callbacksHandler, &CallbacksHandler::messageReceived, this, &ConversationModelPimpl::slotMessageReceived);
    connect(&callbacksHandler, &CallbacksHandler::messageUpdated, this, &ConversationModelPimpl::slotMessageUpdated);
    connect(&callbacksHandler, &CallbacksHandler::reactionAdded, this, &ConversationModelPimpl::slotReactionAdded);
    connect(&callbacksHandler, &CallbacksHandler::reactionRemoved, this, &ConversationModelPimpl::slotReactionRemoved);
    connect(&callbacksHandler,
            &CallbacksHandler::conversationProfileUpdated,
            this,
            &ConversationModelPimpl::slotConversationProfileUpdated);
    connect(&callbacksHandler,
            &CallbacksHandler::conversationRequestReceived,
            this,
            &ConversationModelPimpl::slotConversationRequestReceived);
    connect(&callbacksHandler,
            &CallbacksHandler::conversationRequestDeclined,
            this,
            &ConversationModelPimpl::slotConversationRemoved);
    connect(&callbacksHandler,
            &CallbacksHandler::conversationReady,
            this,
            &ConversationModelPimpl::slotConversationReady);
    connect(&callbacksHandler,
            &CallbacksHandler::conversationRemoved,
            this,
            &ConversationModelPimpl::slotConversationRemoved);
    connect(&callbacksHandler,
            &CallbacksHandler::conversationMemberEvent,
            this,
            &ConversationModelPimpl::slotConversationMemberEvent);
    connect(&callbacksHandler,
            &CallbacksHandler::conversationError,
            this,
            &ConversationModelPimpl::slotOnConversationError);
    connect(&callbacksHandler,
            &CallbacksHandler::conversationPreferencesUpdated,
            this,
            &ConversationModelPimpl::slotConversationPreferencesUpdated);
    connect(&callbacksHandler,
            &CallbacksHandler::activeCallsChanged,
            this,
            &ConversationModelPimpl::slotActiveCallsChanged);
}

ConversationModelPimpl::~ConversationModelPimpl()
{
    // Contact related
    disconnect(&*linked.owner.contactModel,
               &ContactModel::contactUpdated,
               this,
               &ConversationModelPimpl::slotContactUpdated);
    disconnect(&*linked.owner.contactModel,
               &ContactModel::contactAdded,
               this,
               &ConversationModelPimpl::slotContactAdded);
    disconnect(&*linked.owner.contactModel,
               &ContactModel::pendingContactAccepted,
               this,
               &ConversationModelPimpl::slotPendingContactAccepted);
    disconnect(&*linked.owner.contactModel,
               &ContactModel::contactRemoved,
               this,
               &ConversationModelPimpl::slotContactRemoved);

    // Messages related
    disconnect(&*linked.owner.contactModel,
               &lrc::ContactModel::newAccountMessage,
               this,
               &ConversationModelPimpl::slotNewAccountMessage);
    disconnect(&callbacksHandler,
               &CallbacksHandler::incomingCallMessage,
               this,
               &ConversationModelPimpl::slotIncomingCallMessage);
    disconnect(&callbacksHandler,
               &CallbacksHandler::accountMessageStatusChanged,
               this,
               &ConversationModelPimpl::updateInteractionStatus);

    // Call related
    disconnect(&*linked.owner.contactModel, &ContactModel::newCall, this, &ConversationModelPimpl::slotNewCall);
    disconnect(&*linked.owner.callModel,
               &lrc::api::CallModel::callStatusChanged,
               this,
               &ConversationModelPimpl::slotCallStatusChanged);
    disconnect(&*linked.owner.callModel,
               &lrc::api::CallModel::callStarted,
               this,
               &ConversationModelPimpl::slotCallStarted);
    disconnect(&*linked.owner.callModel, &lrc::api::CallModel::callEnded, this, &ConversationModelPimpl::slotCallEnded);
    disconnect(&*linked.owner.callModel,
               &lrc::api::CallModel::callAddedToConference,
               this,
               &ConversationModelPimpl::slotCallAddedToConference);
    disconnect(&callbacksHandler,
               &CallbacksHandler::conferenceRemoved,
               this,
               &ConversationModelPimpl::slotConferenceRemoved);
    disconnect(&ConfigurationManager::instance(),
               &ConfigurationManagerInterface::composingStatusChanged,
               this,
               &ConversationModelPimpl::slotComposingStatusChanged);

    // data transfer
    disconnect(&*linked.owner.contactModel,
               &ContactModel::newAccountTransfer,
               this,
               &ConversationModelPimpl::slotTransferStatusCreated);
    disconnect(&callbacksHandler,
               &CallbacksHandler::transferStatusCanceled,
               this,
               &ConversationModelPimpl::slotTransferStatusCanceled);
    disconnect(&callbacksHandler,
               &CallbacksHandler::transferStatusAwaitingPeer,
               this,
               &ConversationModelPimpl::slotTransferStatusAwaitingPeer);
    disconnect(&callbacksHandler,
               &CallbacksHandler::transferStatusAwaitingHost,
               this,
               &ConversationModelPimpl::slotTransferStatusAwaitingHost);
    disconnect(&callbacksHandler,
               &CallbacksHandler::transferStatusOngoing,
               this,
               &ConversationModelPimpl::slotTransferStatusOngoing);
    disconnect(&callbacksHandler,
               &CallbacksHandler::transferStatusFinished,
               this,
               &ConversationModelPimpl::slotTransferStatusFinished);
    disconnect(&callbacksHandler,
               &CallbacksHandler::transferStatusError,
               this,
               &ConversationModelPimpl::slotTransferStatusError);
    disconnect(&callbacksHandler,
               &CallbacksHandler::transferStatusTimeoutExpired,
               this,
               &ConversationModelPimpl::slotTransferStatusTimeoutExpired);
    disconnect(&callbacksHandler,
               &CallbacksHandler::transferStatusUnjoinable,
               this,
               &ConversationModelPimpl::slotTransferStatusUnjoinable);
    // swarm conversations
    disconnect(&callbacksHandler, &CallbacksHandler::swarmLoaded, this, &ConversationModelPimpl::slotSwarmLoaded);
    disconnect(&callbacksHandler, &CallbacksHandler::messagesFound, this, &ConversationModelPimpl::slotMessagesFound);
    disconnect(&callbacksHandler,
               &CallbacksHandler::messageReceived,
               this,
               &ConversationModelPimpl::slotMessageReceived);
    disconnect(&callbacksHandler, &CallbacksHandler::messageUpdated, this, &ConversationModelPimpl::slotMessageUpdated);
    disconnect(&callbacksHandler, &CallbacksHandler::reactionAdded, this, &ConversationModelPimpl::slotReactionAdded);
    disconnect(&callbacksHandler,
               &CallbacksHandler::reactionRemoved,
               this,
               &ConversationModelPimpl::slotReactionRemoved);
    disconnect(&callbacksHandler,
               &CallbacksHandler::conversationProfileUpdated,
               this,
               &ConversationModelPimpl::slotConversationProfileUpdated);
    disconnect(&callbacksHandler,
               &CallbacksHandler::conversationRequestReceived,
               this,
               &ConversationModelPimpl::slotConversationRequestReceived);
    disconnect(&callbacksHandler,
               &CallbacksHandler::conversationRequestDeclined,
               this,
               &ConversationModelPimpl::slotConversationRemoved);
    disconnect(&callbacksHandler,
               &CallbacksHandler::conversationReady,
               this,
               &ConversationModelPimpl::slotConversationReady);
    disconnect(&callbacksHandler,
               &CallbacksHandler::conversationRemoved,
               this,
               &ConversationModelPimpl::slotConversationRemoved);
    disconnect(&callbacksHandler,
               &CallbacksHandler::conversationMemberEvent,
               this,
               &ConversationModelPimpl::slotConversationMemberEvent);
    disconnect(&callbacksHandler,
               &CallbacksHandler::conversationError,
               this,
               &ConversationModelPimpl::slotOnConversationError);
    disconnect(&callbacksHandler,
               &CallbacksHandler::activeCallsChanged,
               this,
               &ConversationModelPimpl::slotActiveCallsChanged);
    disconnect(&callbacksHandler,
               &CallbacksHandler::conversationPreferencesUpdated,
               this,
               &ConversationModelPimpl::slotConversationPreferencesUpdated);
}

void
ConversationModelPimpl::initConversations()
{
    const MapStringString accountDetails = ConfigurationManager::instance().getAccountDetails(linked.owner.id);
    if (accountDetails.empty())
        return;

    auto isJami = linked.owner.profileInfo.type == profile::Type::JAMI;

    if (isJami) {
        // Fill swarm conversations
        QStringList swarms = ConfigurationManager::instance().getConversations(linked.owner.id);
        for (auto& swarmConv : swarms) {
            addSwarmConversation(swarmConv);
        }

        VectorMapStringString conversationsRequests = ConfigurationManager::instance().getConversationRequests(
            linked.owner.id);
        for (auto& request : conversationsRequests) {
            addConversationRequest(request);
        }

        for (auto const& c : linked.owner.contactModel->getAllContacts()) {
            if (hasOneOneSwarmWith(c))
                continue;
            bool isRequest = c.profileInfo.type == profile::Type::PENDING;
            // Can't find a conversation with this contact
            // add pending not swarm conversation
            if (isRequest) {
                addContactRequest(c.profileInfo.uri);
            }
        }
    } else {
        // Fill conversations
        for (auto const& c : linked.owner.contactModel->getAllContacts().toStdMap()) {
            auto conv = storage::getConversationsWithPeer(db, c.second.profileInfo.uri);
            bool isRequest = c.second.profileInfo.type == profile::Type::PENDING;
            if (conv.empty()) {
                // Can't find a conversation with this contact
                // add pending not swarm conversation
                if (isRequest) {
                    addContactRequest(c.second.profileInfo.uri);
                    continue;
                }
                conv.push_back(storage::beginConversationWithPeer(db,
                                                                  c.second.profileInfo.uri,
                                                                  true,
                                                                  linked.owner.contactModel->getAddedTs(
                                                                      c.second.profileInfo.uri)));
            }
            addConversationWith(conv[0], c.first, isRequest);

            auto convIdx = indexOf(conv[0]);

            // Resolve any file transfer interactions were left in an incorrect state
            auto& interactions = conversations[convIdx].interactions;
            interactions->forEach([&](const QString& id, interaction::Info& interaction) {
                if (interaction.transferStatus == interaction::TransferStatus::TRANSFER_CREATED
                    || interaction.transferStatus == interaction::TransferStatus::TRANSFER_AWAITING_HOST
                    || interaction.transferStatus == interaction::TransferStatus::TRANSFER_AWAITING_PEER
                    || interaction.transferStatus == interaction::TransferStatus::TRANSFER_ONGOING
                    || interaction.transferStatus == interaction::TransferStatus::TRANSFER_ACCEPTED) {
                    // If a datatransfer was left in a non-terminal status in DB, we switch this status
                    // to ERROR
                    // TODO : Improve for DBus clients as daemon and transfer may still be ongoing
                    storage::updateInteractionTransferStatus(db, id, interaction::TransferStatus::TRANSFER_ERROR);

                    interaction.transferStatus = interaction::TransferStatus::TRANSFER_ERROR;
                }
            });
        }
    }
    invalidateModel();

    filteredConversations.reset(conversations).sort();

    // Load all non treated messages for this account
    QVector<Message> messages = ConfigurationManager::instance().getLastMessages(linked.owner.id,
                                                                                 storage::getLastTimestamp(db));
    for (const auto& message : messages) {
        uint64_t timestamp = 0;
        try {
            timestamp = static_cast<uint64_t>(message.received);
        } catch (...) {
        }
        addIncomingMessage(message.from, message.payloads[TEXT_PLAIN], timestamp);
    }
}

const VectorString
ConversationModelPimpl::peersForConversation(const conversation::Info& conversation) const
{
    VectorString result {};
    switch (conversation.mode) {
    case conversation::Mode::NON_SWARM:
        return {conversation.participants[0].uri};
    default:
        break;
    }
    // Note: for one to one, we must return self
    if (conversation.participants.size() == 1)
        return {conversation.participants[0].uri};
    for (const auto& participant : conversation.participants) {
        if (participant.uri.isNull())
            continue;
        if (participant.uri != linked.owner.profileInfo.uri)
            result.push_back(participant.uri);
    }
    return result;
}

bool
ConversationModelPimpl::filter(const conversation::Info& entry)
{
    try {
        // TODO: filter for group?
        // for now group conversation filtered by first peer
        auto& peers = peersForConversation(entry);
        if (peers.size() < 1) {
            return false;
        }
        auto uriPeer = peers.front();
        contact::Info contactInfo;
        try {
            contactInfo = linked.owner.contactModel->getContact(uriPeer);
        } catch (...) {
            // Note: as we search for contacts, when importing a new account,
            // the conversation's request can be there without contact, causing
            // the function to fail.
            contactInfo.profileInfo.uri = uriPeer;
        }

        auto uri = URI(currentFilter);
        bool stripScheme = (uri.schemeType() < URI::SchemeType::COUNT__);
        FlagPack<URI::Section> flags = URI::Section::USER_INFO | URI::Section::HOSTNAME | URI::Section::PORT;
        if (!stripScheme) {
            flags |= URI::Section::SCHEME;
        }

        currentFilter = uri.format(flags);

        // Check contact
        // If contact is banned, only match if filter is a perfect match
        // do not check banned contact for conversation with multiple participants
        if (contactInfo.isBanned && peers.size() == 1) {
            if (currentFilter == "")
                return false;
            return contactInfo.profileInfo.uri == currentFilter || contactInfo.profileInfo.alias == currentFilter
                   || contactInfo.registeredName == currentFilter;
        }

        std::regex regexFilter;
        auto isValidReFilter = true;
        try {
            regexFilter = std::regex(currentFilter.toStdString(), std::regex_constants::icase);
        } catch (std::regex_error&) {
            isValidReFilter = false;
        }

        auto filterUriAndReg = [regexFilter, isValidReFilter](auto contact, auto filter) {
            auto result = contact.profileInfo.uri.contains(filter) || contact.registeredName.contains(filter);
            if (!result) {
                auto regexFound = isValidReFilter
                                      ? (!contact.profileInfo.uri.isEmpty()
                                         && std::regex_search(contact.profileInfo.uri.toStdString(), regexFilter))
                                            || std::regex_search(contact.registeredName.toStdString(), regexFilter)
                                      : false;
                result |= regexFound;
            }
            return result;
        };

        // Check type
        switch (typeFilter) {
        case FilterType::JAMI:
        case FilterType::SIP:
            if (entry.isRequest)
                return false;
            if (contactInfo.profileInfo.type == profile::Type::TEMPORARY)
                return filterUriAndReg(contactInfo, currentFilter);
            break;
        case FilterType::REQUEST:
            if (!entry.isRequest)
                return false;
            break;
        default:
            break;
        }

        // Otherwise perform usual regex search
        bool result = contactInfo.profileInfo.alias.contains(currentFilter);
        if (!result && isValidReFilter)
            result |= std::regex_search(contactInfo.profileInfo.alias.toStdString(), regexFilter);
        if (!result)
            result |= filterUriAndReg(contactInfo, currentFilter);
        return result;
    } catch (std::out_of_range&) {
        // getContact() failed
        return false;
    }
}

bool
ConversationModelPimpl::sort(const conversation::Info& convA, const conversation::Info& convB)
{
    // A or B is a temporary contact
    if (convA.participants.isEmpty())
        return true;
    if (convB.participants.isEmpty())
        return false;

    if (convA.uid == convB.uid)
        return false;

    auto& historyA = convA.interactions;
    auto& historyB = convB.interactions;

    std::lock(historyA->getMutex(), historyB->getMutex());
    std::lock_guard<std::recursive_mutex> lockConvA(historyA->getMutex(), std::adopt_lock);
    std::lock_guard<std::recursive_mutex> lockConvB(historyB->getMutex(), std::adopt_lock);

    // A or B is a new conversation (without CONTACT interaction)
    if (convA.uid.isEmpty() || convB.uid.isEmpty())
        return convA.uid.isEmpty();

    if (historyA->empty() && historyB->empty()) {
        // If no information to compare, sort by Ring ID. For group conversation sort by first peer
        auto& peersForA = peersForConversation(convA);
        auto& peersForB = peersForConversation(convB);
        if (peersForA.isEmpty()) {
            return false;
        }
        if (peersForB.isEmpty()) {
            return true;
        }
        return peersForA.front() > peersForB.front();
    }
    if (historyA->empty())
        return false;
    if (historyB->empty())
        return true;
    // Sort by last Interaction
    time_t timestampA, timestampB;
    historyA->withLast(
        [&](const QString&, const interaction::Info& interaction) { timestampA = interaction.timestamp; });
    historyB->withLast(
        [&](const QString&, const interaction::Info& interaction) { timestampB = interaction.timestamp; });
    return timestampA > timestampB;
}

void
ConversationModelPimpl::sendContactRequest(const QString& contactUri)
{
    try {
        auto contact = linked.owner.contactModel->getContact(contactUri);
        auto isNotUsed = contact.profileInfo.type == profile::Type::TEMPORARY
                         || contact.profileInfo.type == profile::Type::PENDING;
        if (isNotUsed)
            linked.owner.contactModel->addContact(contact);
    } catch (std::out_of_range& e) {
    }
}

void
ConversationModelPimpl::slotSwarmLoaded(uint32_t requestId,
                                        const QString& accountId,
                                        const QString& conversationId,
                                        const VectorSwarmMessage& messages)
{
    if (accountId != linked.owner.id)
        return;
    auto allLoaded = false;
    try {
        auto& conversation = getConversationForUid(conversationId).get();
        for (const auto& message : messages) {
            QString msgId = message.id;
            auto msg = interaction::Info(message, linked.owner.profileInfo.uri, accountId, conversationId);
            auto downloadFile = false;
            if (msg.type == interaction::Type::INITIAL) {
                allLoaded = true;
            } else if (msg.type == interaction::Type::DATA_TRANSFER) {
                QString fileId = message.body.value("fileId");
                linked.owner.dataTransferModel->registerTransferId(fileId, msgId);
                downloadFile = (msg.transferStatus == interaction::TransferStatus::TRANSFER_AWAITING_HOST);
            }

            // If message is loaded, insert message at beginning
            if (!conversation.interactions->insert(msgId, msg, 0)) {
                qDebug() << Q_FUNC_INFO << "Insert failed: duplicate ID.";
                continue;
            }

            if (downloadFile) {
                handleIncomingFile(conversationId, msgId, QString(message.body.value("totalSize")).toInt());
            }
        }

        conversation.lastSelfMessageId = conversation.interactions->lastSelfMessageId(linked.owner.profileInfo.uri);
        invalidateModel();
        Q_EMIT linked.modelChanged();
        Q_EMIT linked.newMessagesAvailable(linked.owner.id, conversationId);
        auto conversationIdx = indexOf(conversationId);
        Q_EMIT linked.dataChanged(conversationIdx);
        Q_EMIT linked.conversationMessagesLoaded(requestId, conversationId);
        if (allLoaded) {
            conversation.allMessagesLoaded = true;
            Q_EMIT linked.conversationUpdated(conversationId);
        }
    } catch (const std::exception& e) {
        qWarning() << e.what();
    }
}

void
ConversationModelPimpl::slotMessagesFound(uint32_t requestId,
                                          const QString& accountId,
                                          const QString& conversationId,
                                          const VectorMapStringString& messageIds)
{
    QMap<QString, interaction::Info> messageDetailedInformation;
    if (requestId == mediaResearchRequestId) {
        Q_FOREACH (const MapStringString& msg, messageIds) {
            auto intInfo = interaction::Info(msg, "", accountId, conversationId);
            messageDetailedInformation[msg["id"]] = std::move(intInfo);
        }
    } else if (requestId == msgResearchRequestId) {
        Q_FOREACH (const MapStringString& msg, messageIds) {
            auto intInfo = interaction::Info(msg, "", accountId, conversationId);
            if (intInfo.type == interaction::Type::TEXT) {
                messageDetailedInformation[msg["id"]] = std::move(intInfo);
            }
        }
    }
    Q_EMIT linked.messagesFoundProcessed(accountId, messageDetailedInformation);
}

void
ConversationModelPimpl::slotMessageReceived(const QString& accountId,
                                            const QString& conversationId,
                                            const SwarmMessage& message)
{
    if (accountId != linked.owner.id)
        return;
    try {
        auto& conversation = getConversationForUid(conversationId).get();
        if (message.type == "initial") {
            conversation.allMessagesLoaded = true;
            Q_EMIT linked.conversationUpdated(conversationId);
            if (message.body.find("invited") == message.body.end()) {
                return;
            }
        }
        QString msgId = message.id;
        auto msg = interaction::Info(message, linked.owner.profileInfo.uri, accountId, conversationId);

        if (msg.type == interaction::Type::CALL) {
            msg.body = interaction::getCallInteractionString(msg.authorUri == linked.owner.profileInfo.uri, msg);
        } else if (msg.type == interaction::Type::DATA_TRANSFER) {
            QString fileId = message.body.value("fileId");
            linked.owner.dataTransferModel->registerTransferId(fileId, msgId);
        }

        if (!conversation.interactions->append(msgId, msg)) {
            qDebug() << Q_FUNC_INFO << "Append failed: duplicate ID." << msgId;
            return;
        }

        auto updateUnread = msg.authorUri != linked.owner.profileInfo.uri;
        if (updateUnread)
            conversation.unreadMessages++;
        conversation.lastSelfMessageId = conversation.interactions->lastSelfMessageId(linked.owner.profileInfo.uri);
        invalidateModel();
        if (!interaction::isOutgoing(msg) && updateUnread) {
            Q_EMIT behaviorController.newUnreadInteraction(linked.owner.id, conversationId, msgId, msg);
        }
        Q_EMIT linked.newInteraction(conversationId, msgId, msg);
        Q_EMIT linked.modelChanged();
        if (msg.transferStatus == interaction::TransferStatus::TRANSFER_AWAITING_HOST) {
            handleIncomingFile(conversationId, msgId, QString(message.body.value("totalSize")).toInt());
        }
        Q_EMIT linked.dataChanged(indexOf(conversationId));
        // Update status
        using namespace libjami::Account;
        for (const auto& uri : message.status.keys()) {
            if (uri == linked.owner.profileInfo.uri)
                continue;
            if (message.status.value(uri) == static_cast<int>(MessageStates::DISPLAYED))
                conversation.interactions->setRead(uri, message.id);
        }
    } catch (const std::exception& e) {
        qDebug() << "Messages received for conversation that does not exist.";
    }
}

void
ConversationModelPimpl::slotMessageUpdated(const QString& accountId,
                                           const QString& conversationId,
                                           const SwarmMessage& message)
{
    if (accountId != linked.owner.id)
        return;
    try {
        auto& conversation = getConversationForUid(conversationId).get();
        QString msgId = message.id;
        auto msg = interaction::Info(message, linked.owner.profileInfo.uri, accountId, conversationId);

        if (!conversation.interactions->update(msgId, msg)) {
            qDebug() << "Message not found or cannot be reparented.";
            return;
        }
        // The conversation is updated, so we need to notify the view.
        invalidateModel();
        Q_EMIT linked.modelChanged();
        Q_EMIT linked.dataChanged(indexOf(conversationId));
    } catch (const std::exception& e) {
        qDebug() << "Messages received for conversation that does not exist.";
    }
}

void
ConversationModelPimpl::slotReactionAdded(const QString& accountId,
                                          const QString& conversationId,
                                          const QString& messageId,
                                          const MapStringString& reaction)
{
    if (accountId != linked.owner.id) {
        return;
    }
    try {
        // qInfo() << "Add Reaction to " << messageId << " in " << conversationId;
        auto& conversation = getConversationForUid(conversationId).get();
        conversation.interactions->addReaction(messageId, reaction);
    } catch (const std::exception& e) {
        qWarning() << e.what();
    }
}

void
ConversationModelPimpl::slotReactionRemoved(const QString& accountId,
                                            const QString& conversationId,
                                            const QString& messageId,
                                            const QString& reactionId)
{
    if (accountId != linked.owner.id) {
        return;
    }
    try {
        // qInfo() << "Remove Reaction from " << messageId << " in " << conversationId;
        auto& conversation = getConversationForUid(conversationId).get();
        conversation.interactions->rmReaction(messageId, reactionId);
    } catch (const std::exception& e) {
        qWarning() << e.what();
    }
}

void
ConversationModelPimpl::slotConversationProfileUpdated(const QString& accountId,
                                                       const QString& conversationId,
                                                       const MapStringString& profile)
{
    if (accountId != linked.owner.id) {
        return;
    }
    try {
        auto& conversation = getConversationForUid(conversationId).get();
        conversation.infos = profile;
        Q_EMIT linked.profileUpdated(conversationId);
    } catch (...) {
    }
}

void
ConversationModelPimpl::slotConversationRequestReceived(const QString& accountId,
                                                        const QString&,
                                                        const MapStringString& metadatas)
{
    if (accountId != linked.owner.id)
        return;
    addConversationRequest(metadatas, true);
}

void
ConversationModelPimpl::slotConversationReady(const QString& accountId, const QString& conversationId)
{
    // we receive this signal after we accept or after we send a conversation request
    if (accountId != linked.owner.id) {
        return;
    }
    // remove non swarm conversation that was added from slotContactAdded
    const VectorMapStringString& members = ConfigurationManager::instance().getConversationMembers(accountId,
                                                                                                   conversationId);
    QVector<member::Member> participants;
    // it means conversation with one participant. In this case we could have non swarm conversation
    bool shouldRemoveNonSwarmConversation = members.size() == 2;
    for (const auto& member : members) {
        participants.append({member["uri"], api::member::to_role(member["role"])});
        if (shouldRemoveNonSwarmConversation) {
            try {
                auto& conversation = getConversationForPeerUri(member["uri"]).get();
                // remove non swarm conversation
                if (conversation.isLegacy()) {
                    eraseConversation(conversation.uid);
                    storage::removeContactConversations(db, member["uri"]);
                    invalidateModel();
                    Q_EMIT linked.conversationRemoved(conversation.uid);
                    Q_EMIT linked.modelChanged();
                }
            } catch (...) {
            }
        }
    }

    auto conversationIt = conversationMap.find(conversationId);
    bool conversationExists = conversationIt != conversationMap.end();

    if (!conversationExists) {
        addSwarmConversation(conversationId);
        conversationIt = conversationMap.find(conversationId);
        if (conversationIt == conversationMap.end()) {
            return;
        }
    }
    auto& conversation = conversations.at(conversationIt->second);
    if (conversationExists) {
        // if swarm request already exists, update participnts
        conversation.participants = participants;
        const MapStringString& details = ConfigurationManager::instance().conversationInfos(accountId, conversationId);
        conversation.infos = details;
        const MapStringString& preferences = ConfigurationManager::instance().getConversationPreferences(accountId,
                                                                                                         conversationId);
        conversation.preferences = preferences;
        conversation.mode = conversation::to_mode(details["mode"].toInt());
        conversation.isRequest = false;
        conversation.needsSyncing = false;
        Q_EMIT linked.conversationUpdated(conversationId);
        Q_EMIT linked.dataChanged(indexOf(conversationId));
        ConfigurationManager::instance().loadConversation(linked.owner.id, conversationId, "", 0);
        auto& peers = peersForConversation(conversation);
        if (peers.size() == 1)
            Q_EMIT linked.conversationReady(conversationId, peers.front());
        return;
    }
    invalidateModel();
    // we use conversationReady callback only for conversation with one participant. We could use
    // participants.front()
    auto& peers = peersForConversation(conversation);
    if (peers.size() == 1)
        Q_EMIT linked.conversationReady(conversationId, peers.front());
    Q_EMIT linked.newConversation(conversationId);
    Q_EMIT linked.modelChanged();
}

void
ConversationModelPimpl::slotConversationRemoved(const QString& accountId, const QString& conversationId)
{
    if (accountId != linked.owner.id)
        return;
    try {
        eraseConversation(conversationId);
        invalidateModel();
        Q_EMIT linked.conversationRemoved(conversationId);
    } catch (const std::exception& e) {
        qWarning() << e.what();
    }
}

void
ConversationModelPimpl::slotConversationMemberEvent(const QString& accountId,
                                                    const QString& conversationId,
                                                    const QString& memberUri,
                                                    int event)
{
    if (accountId != linked.owner.id || indexOf(conversationId) < 0) {
        return;
    }
    if (event == 0 /* add */) {
        // clear search result
        for (unsigned int i = 0; i < searchResults.size(); ++i) {
            if (searchResults.at(i).uid == memberUri)
                searchResults.erase(searchResults.begin() + i);
        }
    }
    // update participants
    try {
        auto& conversation = getConversationForUid(conversationId).get();
        const VectorMapStringString& members = ConfigurationManager::instance().getConversationMembers(linked.owner.id,
                                                                                                       conversationId);
        QVector<member::Member> participants;
        VectorString membersRemaining;
        for (auto& member : members) {
            participants.append(member::Member {member["uri"], member::to_role(member["role"])});
            if (member["role"] != "left")
                membersRemaining.append(member["uri"]);
        }
        conversation.participants = participants;
        invalidateModel();
    } catch (...) {
    }
    Q_EMIT linked.modelChanged();
    Q_EMIT linked.conversationUpdated(conversationId);
    Q_EMIT linked.dataChanged(indexOf(conversationId));
}

void
ConversationModelPimpl::slotOnConversationError(const QString& accountId,
                                                const QString& conversationId,
                                                int code,
                                                const QString& what)
{
    if (accountId != linked.owner.id || indexOf(conversationId) < 0) {
        return;
    }
    try {
        auto& conversation = getConversationForUid(conversationId).get();
        conversation.errors.push_back({code, what});
        Q_EMIT linked.conversationErrorsUpdated(conversationId);
    } catch (...) {
    }
}

void
ConversationModelPimpl::slotActiveCallsChanged(const QString& accountId,
                                               const QString& conversationId,
                                               const VectorMapStringString& activeCalls)
{
    if (accountId != linked.owner.id || indexOf(conversationId) < 0) {
        return;
    }
    try {
        auto& conversation = getConversationForUid(conversationId).get();
        conversation.activeCalls = activeCalls;
        if (activeCalls.empty())
            conversation.ignoredActiveCalls.clear();
        Q_EMIT linked.activeCallsChanged(accountId, conversationId);
    } catch (...) {
    }
}

void
ConversationModelPimpl::slotContactAdded(const QString& contactUri)
{
    QString convId;
    try {
        convId = linked.owner.contactModel->getContact(contactUri).conversationId;
    } catch (std::out_of_range& e) {
        return;
    }
    auto isSip = linked.owner.profileInfo.type == profile::Type::SIP;
    auto isSwarm = !convId.isEmpty();
    auto conv = !isSwarm ? (isSip ? storage::getConversationsWithPeer(db, contactUri) : VectorString {})
                         : VectorString {convId};
    if (conv.isEmpty()) {
        if (isSip) {
            auto convId = storage::beginConversationWithPeer(db,
                                                             contactUri,
                                                             true,
                                                             linked.owner.contactModel->getAddedTs(contactUri));
            addConversationWith(convId, contactUri, false);
            Q_EMIT linked.conversationReady(convId, contactUri);
            Q_EMIT linked.newConversation(convId);
        }
        return;
    }
    convId = conv[0];
    try {
        auto& conversation = getConversationForUid(convId).get();
        MapStringString details = ConfigurationManager::instance().conversationInfos(linked.owner.id, conversation.uid);
        bool needsSyncing = details["syncing"] == "true";
        if (conversation.needsSyncing != needsSyncing) {
            conversation.isRequest = false;
            conversation.needsSyncing = needsSyncing;
            Q_EMIT linked.dataChanged(indexOf(conversation.uid));
            Q_EMIT linked.conversationUpdated(conversation.uid);
            invalidateModel();
            Q_EMIT linked.modelChanged();
        }
    } catch (...) {
        if (isSwarm) {
            addSwarmConversation(convId);
        }
    }
}

void
ConversationModelPimpl::addContactRequest(const QString& contactUri)
{
    try {
        getConversationForPeerUri(contactUri).get();
        // request from contact already exists, return
        return;
    } catch (std::out_of_range&) {
        // no conversation exists. Add contact request
        conversation::Info conversation(contactUri, &linked.owner);
        conversation.participants = {{contactUri, member::Role::INVITED}};
        conversation.mode = conversation::Mode::NON_SWARM;
        conversation.isRequest = true;
        emplaceBackConversation(std::move(conversation));
        invalidateModel();
        Q_EMIT linked.newConversation(contactUri);
        Q_EMIT linked.modelChanged();
    }
}

void
ConversationModelPimpl::addConversationRequest(const MapStringString& convRequest, bool emitToClient)
{
    auto convId = convRequest["id"];
    auto convIdx = indexOf(convId);
    if (convIdx != -1)
        return;

    auto peerUri = convRequest["from"];
    auto mode = conversation::to_mode(convRequest["mode"].toInt());
    QString callId, confId;
    const MapStringString& details = ConfigurationManager::instance().conversationInfos(linked.owner.id, convId);
    conversation::Info conversation(convId, &linked.owner);
    conversation.infos = details;
    conversation.callId = callId;
    conversation.confId = confId;
    conversation.participants = {{linked.owner.profileInfo.uri, member::Role::INVITED}, {peerUri, member::Role::MEMBER}};
    conversation.mode = mode;
    conversation.isRequest = true;

    MapStringString messageMap = {
        {"type", "initial"},
        {"mode", QString::number(static_cast<int>(mode))},
        {"author", peerUri},
        {"timestamp", convRequest["received"]},
        {"linearizedParent", ""},
    };
    auto msg = interaction::Info(messageMap, linked.owner.profileInfo.uri);
    conversation.interactions->insert(convId, msg, 0);

    // add the author to the contact model's contact list as a PENDING
    // if they aren't already a contact
    auto isSelf = linked.owner.profileInfo.uri == peerUri;
    if (isSelf)
        return;

    if (mode == conversation::Mode::ONE_TO_ONE) {
        try {
            profile::Info profileInfo;
            profileInfo.uri = peerUri;
            profileInfo.type = profile::Type::JAMI;
            profileInfo.alias = details["title"];
            profileInfo.avatar = details["avatar"];
            contact::Info contactInfo;
            contactInfo.profileInfo = profileInfo;
            linked.owner.contactModel->addContact(contactInfo);
        } catch (std::out_of_range&) {
            qWarning() << "Couldn't find contact request conversation for" << peerUri;
        }
    }

    emplaceBackConversation(std::move(conversation));
    invalidateModel();
    Q_EMIT linked.newConversation(convId);
    Q_EMIT linked.modelChanged();
    if (!callId.isEmpty()) {
        // If we replace a non swarm request by a swarm request while having a call.
        linked.selectConversation(convId);
    }
    if (emitToClient)
        Q_EMIT behaviorController.newTrustRequest(linked.owner.id, convId, peerUri);
}

void
ConversationModelPimpl::slotPendingContactAccepted(const QString& uri)
{
    profile::Type type = profile::Type::INVALID;
    try {
        type = linked.owner.contactModel->getContact(uri).profileInfo.type;
    } catch (std::out_of_range& e) {
    }
    profile::Info profileInfo {uri, {}, {}, type};
    storage::vcard::setProfile(linked.owner.id, profileInfo, true);
    auto isSip = linked.owner.profileInfo.type == profile::Type::SIP;
    auto convs = isSip ? storage::getConversationsWithPeer(db, uri) : VectorString {};
    if (!convs.empty()) {
        try {
            auto contact = linked.owner.contactModel->getContact(uri);
            auto interaction = interaction::Info {uri,
                                                  {},
                                                  std::time(nullptr),
                                                  0,
                                                  interaction::Type::CONTACT,
                                                  interaction::Status::SUCCESS,
                                                  true};
            auto msgId = storage::addMessageToConversation(db, convs[0], interaction);
            interaction.body = storage::getContactInteractionString(uri, interaction::Status::SUCCESS);
            auto convIdx = indexOf(convs[0]);
            if (convIdx >= 0) {
                conversations[convIdx].interactions->append(msgId, interaction);
            }
            filteredConversations.invalidate();
            Q_EMIT linked.newInteraction(convs[0], msgId, interaction);
            Q_EMIT linked.dataChanged(convIdx);
        } catch (std::out_of_range& e) {
            qDebug() << "ConversationModelPimpl::slotContactAdded cannot find contact.";
        }
    }
}

void
ConversationModelPimpl::slotContactRemoved(const QString& uri)
{
    std::vector<QString> convIdsToRemove;

    // save the ids to remove from the list
    for (auto i : getIndicesForContact(uri)) {
        convIdsToRemove.emplace_back(conversations[i].uid);
    }

    // actually remove them from the list
    for (const auto& id : convIdsToRemove) {
        eraseConversation(id);
        Q_EMIT linked.conversationRemoved(id);
    }

    invalidateModel();
    Q_EMIT linked.modelChanged();
}

void
ConversationModelPimpl::slotContactUpdated(const QString& uri)
{
    // Update all conversations with this peer
    for (auto& conversation : conversations) {
        auto members = peersForConversation(conversation);
        if (members.indexOf(uri) != -1) {
            invalidateModel();
            Q_EMIT linked.conversationUpdated(conversation.uid);
            Q_EMIT linked.dataChanged(indexOf(conversation.uid));
        }
    }

    // TODO: investigate and possibly refactor the following search list management.
    // Might just need comments to clarify the intent.
    if (currentFilter.isEmpty()) {
        if (searchResults.empty())
            return;
        searchResults.clear();
        Q_EMIT linked.searchResultUpdated();
        return;
    }

    searchResults.clear();
    auto users = linked.owner.contactModel->getSearchResults();
    for (auto& user : users) {
        auto uid = linked.owner.profileInfo.type == profile::Type::SIP ? "SEARCHSIP" : user.profileInfo.uri;
        conversation::Info conversationInfo(uid, &linked.owner);
        // For SIP, we always got one search result, so "" is ok as there is no empty uri
        // For Jami accounts, the nameserver can return several results, so we use the uniqueness of
        // the id as id for a temporary conversation.
        conversationInfo.participants.append(member::Member {user.profileInfo.uri, member::Role::MEMBER});
        searchResults.emplace_front(std::move(conversationInfo));
    }
    Q_EMIT linked.searchResultUpdated();
    Q_EMIT linked.searchResultEnded();
}

void
ConversationModelPimpl::addSwarmConversation(const QString& convId)
{
    if (Lrc::dbusIsValid()) {
        // Because the daemon may have already loaded interactions
        // we clear them to receive all signals
        ConfigurationManager::instance().clearCache(linked.owner.id, convId);
    }
    QVector<member::Member> participants;
    const VectorMapStringString& members = ConfigurationManager::instance().getConversationMembers(linked.owner.id,
                                                                                                   convId);
    auto accountURI = linked.owner.profileInfo.uri;
    QString otherMember;
    const MapStringString& details = ConfigurationManager::instance().conversationInfos(linked.owner.id, convId);
    auto mode = conversation::to_mode(details["mode"].toInt());
    conversation::Info conversation(convId, &linked.owner);
    conversation.infos = details;
    VectorMapStringString activeCalls = ConfigurationManager::instance().getActiveCalls(linked.owner.id, convId);
    conversation.activeCalls = activeCalls;
    QString lastRead;
    VectorString membersLeft;
    for (auto& member : members) {
        // this check should be removed once all usage of participants replaced by
        // peersForConversation. We should have ourself in participants list
        // Note: if members.size() == 1, it's a conv with self so we're also the peer
        participants.append(member::Member {member["uri"], member::to_role(member["role"])});
        if (mode == conversation::Mode::ONE_TO_ONE && member["uri"] != accountURI) {
            otherMember = member["uri"];
        } else if (member["uri"] == accountURI) {
            lastRead = member["lastDisplayed"];
        } else if (mode != conversation::Mode::ONE_TO_ONE) {
            // Note: a conversation may be with non contacts.
            // So, refresh bestName to be sure to get quick updates in UI
            linked.owner.contactModel->bestNameForContact(member["uri"]);
        }
        if (member["uri"] != accountURI)
            conversation.interactions->setRead(member["uri"], member["lastDisplayed"]);
        if (member["role"] == "left")
            membersLeft.append(member["uri"]);
    }
    conversation.participants = participants;
    conversation.mode = mode;
    const MapStringString& preferences = ConfigurationManager::instance().getConversationPreferences(linked.owner.id,
                                                                                                     convId);
    conversation.preferences = preferences;
    conversation.unreadMessages = ConfigurationManager::instance().countInteractions(linked.owner.id,
                                                                                     convId,
                                                                                     lastRead,
                                                                                     "",
                                                                                     accountURI);
    if (mode == conversation::Mode::ONE_TO_ONE && !otherMember.isEmpty()) {
        try {
            conversation.confId = linked.owner.callModel->getConferenceFromURI(otherMember).id;
        } catch (...) {
            conversation.confId = "";
        }
        try {
            conversation.callId = linked.owner.callModel->getCallFromURI(otherMember).id;
        } catch (...) {
            conversation.callId = "";
        }
    }
    // If conversation has only one peer it is possible that non swarm conversation was created.
    // remove non swarm conversation
    auto& peers = peersForConversation(conversation);
    if (peers.size() == 1) {
        try {
            auto& participantId = peers.front();
            auto& conv = getConversationForPeerUri(participantId).get();
            if (conv.mode == conversation::Mode::NON_SWARM) {
                eraseConversation(conv.uid);
                invalidateModel();
                Q_EMIT linked.conversationRemoved(conv.uid);
                storage::removeContactConversations(db, participantId);
            }
        } catch (...) {
        }
    }
    if (details["syncing"] == "true") {
        MapStringString messageMap = {
            {"type", "initial"},
            {"author", otherMember},
            {"timestamp", details["created"]},
            {"linearizedParent", ""},
        };
        auto msg = interaction::Info(messageMap, linked.owner.profileInfo.uri);
        conversation.interactions->append(convId, msg);
        conversation.needsSyncing = true;
        Q_EMIT linked.conversationUpdated(conversation.uid);
        Q_EMIT linked.dataChanged(indexOf(conversation.uid));
    }
    emplaceBackConversation(std::move(conversation));
    ConfigurationManager::instance().loadConversation(linked.owner.id, convId, "", 1);
}

void
ConversationModelPimpl::addConversationWith(const QString& convId, const QString& contactUri, bool isRequest)
{
    conversation::Info conversation(convId, &linked.owner);
    conversation.participants = {{contactUri, member::Role::MEMBER}};
    conversation.mode = conversation::Mode::NON_SWARM;
    conversation.needsSyncing = false;
    conversation.isRequest = isRequest;

    try {
        conversation.confId = linked.owner.callModel->getConferenceFromURI(contactUri).id;
    } catch (...) {
        conversation.confId = "";
    }
    try {
        conversation.callId = linked.owner.callModel->getCallFromURI(contactUri).id;
    } catch (...) {
        conversation.callId = "";
    }
    auto isSip = linked.owner.profileInfo.type == profile::Type::SIP;
    if (isSip) {
        storage::getHistory(db, conversation, linked.owner.profileInfo.uri);

        QList<std::function<void(void)>> toUpdate;
        conversation.interactions->forEach([&](const QString& id, interaction::Info& interaction) {
            if (interaction.status != interaction::Status::SENDING) {
                return;
            }
            // Get the message status from daemon, else unknown
            auto daemonId = storage::getDaemonIdByInteractionId(db, id);
            int status = 0;
            if (daemonId.isEmpty()) {
                return;
            }
            try {
                auto msgId = std::stoull(daemonId.toStdString());
                status = ConfigurationManager::instance().getMessageStatus(msgId);
                toUpdate.emplace_back([this, convId, contactUri, daemonId, status]() {
                    auto accId = linked.owner.id;
                    updateInteractionStatus(accId, convId, contactUri, daemonId, status);
                });
            } catch (const std::exception& e) {
                qWarning() << Q_FUNC_INFO << "Failed: message id was invalid";
            }
        });
        Q_FOREACH (const auto& func, toUpdate)
            func();
    }

    conversation.unreadMessages = getNumberOfUnreadMessagesFor(convId);

    emplaceBackConversation(std::move(conversation));
    invalidateModel();
}

int
ConversationModelPimpl::indexOf(const QString& uid) const
{
    auto it = conversationMap.find(uid);
    if (it != conversationMap.end()) {
        return it->second;
    }
    return -1;
}

std::reference_wrapper<conversation::Info>
ConversationModelPimpl::getConversation(const FilterPredicate& pred, const bool searchResultIncluded) const
{
    auto conv = std::find_if(conversations.cbegin(), conversations.cend(), pred);
    if (conv != conversations.cend()) {
        return std::remove_const_t<conversation::Info&>(*conv);
    }

    if (searchResultIncluded) {
        auto sr = std::find_if(searchResults.cbegin(), searchResults.cend(), pred);
        if (sr != searchResults.cend()) {
            return std::remove_const_t<conversation::Info&>(*sr);
        }
    }

    throw std::out_of_range("Conversation not found");
}

std::reference_wrapper<conversation::Info>
ConversationModelPimpl::getConversationForUid(const QString& uid, const bool searchResultIncluded) const
{
    auto it = conversationMap.find(uid);
    if (it != conversationMap.end())
        return std::remove_const_t<conversation::Info&>(conversations.at(it->second));
    if (searchResultIncluded) {
        auto sr = std::find_if(searchResults.begin(), searchResults.end(), [&](const auto& conv) {
            return conv.uid == uid;
        });
        if (sr != searchResults.end()) {
            return std::remove_const_t<conversation::Info&>(*sr);
        }
    }
    throw std::out_of_range("Conversation not found");
}

std::reference_wrapper<conversation::Info>
ConversationModelPimpl::getConversationForPeerUri(const QString& uri, const bool searchResultIncluded) const
{
    return getConversation(
        [this, uri](const conversation::Info& conv) -> bool {
            if (!conv.isCoreDialog()) {
                return false;
            }
            auto members = peersForConversation(conv);
            if (members.isEmpty())
                return false;
            return members.indexOf(uri) != -1;
        },
        searchResultIncluded);
}

std::vector<int>
ConversationModelPimpl::getIndicesForContact(const QString& uri) const
{
    std::vector<int> ret;
    for (unsigned int i = 0; i < conversations.size(); ++i) {
        const auto& convInfo = conversations.at(i);
        if (!convInfo.isCoreDialog()) {
            continue;
        }
        auto peers = peersForConversation(convInfo);
        if (!peers.isEmpty() && peers.front() == uri) {
            ret.emplace_back(i);
        }
    }
    return ret;
}

void
ConversationModelPimpl::slotNewCall(const QString& fromId, const QString& callId, bool isOutgoing, const QString& toUri)
{
    if (isOutgoing) {
        // search contact
        currentFilter = fromId;
        invalidateModel();
        searchResults.clear();
        Q_EMIT linked.searchResultUpdated();
        linked.owner.contactModel->searchContact(currentFilter);
        Q_EMIT linked.filterChanged();
    }

    if (toUri == linked.owner.profileInfo.uri) {
        auto isSip = linked.owner.profileInfo.type == profile::Type::SIP;
        auto convIds = isSip ? storage::getConversationsWithPeer(db, fromId) : VectorString {};
        if (convIds.empty()) {
            // in case if we receive call after removing contact add conversation request;
            try {
                auto contact = linked.owner.contactModel->getContact(fromId);
                if (!isOutgoing && !contact.isBanned && fromId != linked.owner.profileInfo.uri) {
                    addContactRequest(fromId);
                }
                if (isOutgoing && contact.profileInfo.type == profile::Type::TEMPORARY) {
                    linked.owner.contactModel->addContact(contact);
                }
            } catch (const std::out_of_range&) {
            }
        }

        auto conversationIndices = getIndicesForContact(fromId);
        if (conversationIndices.empty()) {
            qDebug() << "ConversationModelPimpl::slotNewCall, but conversation not found.";
            return; // Not a contact
        }

        auto& conversation = conversations.at(conversationIndices.at(0));
        conversation.callId = callId;

        addOrUpdateCallMessage(callId, fromId, true);
        Q_EMIT behaviorController.showIncomingCallView(linked.owner.id, conversation.uid);
    }
}

void
ConversationModelPimpl::slotCallStatusChanged(const QString& accountId, const QString& callId, int code)
{
    Q_UNUSED(code)
    // Get conversation
    auto i = std::find_if(conversations.begin(), conversations.end(), [callId](const conversation::Info& conversation) {
        return conversation.callId == callId;
    });

    try {
        auto accountProperties = lrc.getAccountModel().getAccountConfig(accountId);
        auto call = linked.owner.callModel->getCall(callId);
        if (i != conversations.end()
            && (!accountProperties.denySecondCall || call.status == call::Status::IN_PROGRESS)) {
            // Update interaction status
            invalidateModel();
            linked.selectConversation(i->uid);
            Q_EMIT linked.conversationUpdated(i->uid);
            Q_EMIT linked.dataChanged(indexOf(i->uid));
        }
    } catch (std::out_of_range& e) {
        qDebug() << "ConversationModelPimpl::slotCallStatusChanged cannot get nonexistent call.";
    }
}

void
ConversationModelPimpl::slotCallStarted(const QString& callId)
{
    try {
        auto call = linked.owner.callModel->getCall(callId);
        addOrUpdateCallMessage(callId, call.peerUri.remove("ring:"), !call.isOutgoing);
    } catch (std::out_of_range& e) {
        qDebug() << "ConversationModelPimpl::slotCallStarted cannot start nonexistent call.";
    }
}

void
ConversationModelPimpl::slotCallEnded(const QString& callId)
{
    try {
        auto call = linked.owner.callModel->getCall(callId);
        // get duration
        std::time_t duration = 0;
        if (call.startTime.time_since_epoch().count() != 0) {
            auto duration_ns = std::chrono::steady_clock::now() - call.startTime;
            duration = std::chrono::duration_cast<std::chrono::seconds>(duration_ns).count();
        }
        // add or update call interaction with duration
        addOrUpdateCallMessage(callId, call.peerUri.remove("ring:"), !call.isOutgoing, duration);
        /* Reset the callId stored in the conversation.
           Do not call selectConversation() since it is already done in slotCallStatusChanged. */
        size_t idx = 0;
        for (auto& conversation : conversations) {
            if (conversation.callId == callId) {
                conversation.callId = "";
                conversation.confId = ""; // The participant is detached
                invalidateModel();
                Q_EMIT linked.conversationUpdated(conversation.uid);
                Q_EMIT linked.dataChanged(idx);
            }
            ++idx;
        }
    } catch (std::out_of_range& e) {
        qDebug() << "ConversationModelPimpl::slotCallEnded cannot end nonexistent call.";
    }
}

void
ConversationModelPimpl::addOrUpdateCallMessage(const QString& callId,
                                               const QString& from,
                                               bool incoming,
                                               const std::time_t& duration)
{
    auto isSip = linked.owner.profileInfo.type == profile::Type::SIP;
    // Get conversation
    auto conv_it = std::find_if(conversations.begin(),
                                conversations.end(),
                                [&callId](const conversation::Info& conversation) {
                                    return conversation.callId == callId;
                                });
    if (conv_it == conversations.end()) {
        // If we have no conversation with peer.
        try {
            auto contact = linked.owner.contactModel->getContact(from);
            if (contact.profileInfo.type == profile::Type::PENDING) {
                addContactRequest(from);
                if (isSip) {
                    storage::beginConversationWithPeer(db, contact.profileInfo.uri);
                }
            }
        } catch (const std::exception&) {
            return;
        }
        try {
            auto& conv = getConversationForPeerUri(from).get();
            if (!conv.isSwarm() && conv.callId.isEmpty()) {
                conv.callId = callId;
            }
        } catch (...) {
            return;
        }
    }
    if (!isSip || conv_it == conversations.end() || conv_it->isSwarm())
        return;
    auto uriString = incoming ? storage::prepareUri(from, linked.owner.profileInfo.type) : linked.owner.profileInfo.uri;
    auto msg = interaction::Info {uriString,
                                  {},
                                  std::time(nullptr),
                                  duration,
                                  interaction::Type::CALL,
                                  interaction::Status::SUCCESS,
                                  true};
    // update the db
    auto msgId = storage::addOrUpdateMessage(db, conv_it->uid, msg, callId);
    // now set the formatted call message string in memory only
    msg.body = interaction::getCallInteractionString(msg.authorUri == linked.owner.profileInfo.uri, msg);
    auto [added, success] = conv_it->interactions->addOrUpdate(msgId, msg);
    if (!success) {
        qWarning() << Q_FUNC_INFO << QString("Failed: to %1 msg").arg(added ? "add" : "update");
        return;
    }
    if (added)
        Q_EMIT linked.newInteraction(conv_it->uid, msgId, msg);

    invalidateModel();
    Q_EMIT linked.modelChanged();
    Q_EMIT linked.dataChanged(static_cast<int>(std::distance(conversations.begin(), conv_it)));
}

void
ConversationModelPimpl::slotNewAccountMessage(const QString& accountId,
                                              const QString& peerId,
                                              const QString& msgId,
                                              const MapStringString& payloads)
{
    if (accountId != linked.owner.id)
        return;

    for (auto it = payloads.constBegin(); it != payloads.constEnd(); ++it) {
        const auto& payload = it.key();
        if (payload.contains(TEXT_PLAIN)) {
            addIncomingMessage(peerId, it.value(), 0, msgId);
        } else {
            qDebug() << payload;
        }
    }
}

void
ConversationModelPimpl::slotIncomingCallMessage(const QString& accountId,
                                                const QString& callId,
                                                const QString& from,
                                                const QString& body)
{
    if (accountId != linked.owner.id || !linked.owner.callModel->hasCall(callId))
        return;

    auto& call = linked.owner.callModel->getCall(callId);
    if (call.type == call::Type::CONFERENCE) {
        // Show messages in all conversations for conferences.
        for (const auto& conversation : conversations) {
            if (conversation.confId == callId) {
                if (conversation.participants.empty()) {
                    continue;
                }
                addIncomingMessage(from, body);
            }
        }
    } else {
        addIncomingMessage(from, body);
    }
}

QString
ConversationModelPimpl::addIncomingMessage(const QString& peerId,
                                           const QString& body,
                                           const uint64_t& timestamp,
                                           const QString& daemonId)
{
    if (linked.owner.profileInfo.type != profile::Type::SIP)
        return "";
    auto convIds = storage::getConversationsWithPeer(db, peerId);
    if (convIds.empty()) {
        // in case if we receive a message after removing contact, add a conversation request
        try {
            auto contact = linked.owner.contactModel->getContact(peerId);
            convIds.push_back(storage::beginConversationWithPeer(db, contact.profileInfo.uri));
            auto& conv = getConversationForPeerUri(contact.profileInfo.uri).get();
            conv.uid = convIds[0];
        } catch (const std::out_of_range&) {
            return "";
        }
    }
    auto msg = interaction::Info {peerId,
                                  body,
                                  timestamp == 0 ? std::time(nullptr) : static_cast<time_t>(timestamp),
                                  0,
                                  interaction::Type::TEXT,
                                  interaction::Status::SUCCESS,
                                  false};
    auto msgId = storage::addMessageToConversation(db, convIds[0], msg);
    if (!daemonId.isEmpty()) {
        storage::addDaemonMsgId(db, msgId, daemonId);
    }
    auto conversationIdx = indexOf(convIds[0]);
    // Add the conversation if not already here
    if (conversationIdx == -1) {
        addConversationWith(convIds[0], peerId, false);
        Q_EMIT linked.newConversation(convIds[0]);
    } else {
        // Maybe check if this is failing?
        conversations[conversationIdx].interactions->append(msgId, msg);
        conversations[conversationIdx].unreadMessages = getNumberOfUnreadMessagesFor(convIds[0]);
    }

    Q_EMIT behaviorController.newUnreadInteraction(linked.owner.id, convIds[0], msgId, msg);
    Q_EMIT linked.newInteraction(convIds[0], msgId, msg);

    invalidateModel();
    Q_EMIT linked.modelChanged();
    Q_EMIT linked.dataChanged(conversationIdx);

    return msgId;
}

void
ConversationModelPimpl::slotCallAddedToConference(const QString& callId,
                                                  const QString& conversationId,
                                                  const QString& confId)
{
    for (auto& conversation : conversations) {
        if ((conversationId == conversation.uid)
            || (!callId.isEmpty() && conversation.callId == callId && conversation.confId != confId)) {
            conversation.confId = confId;
            invalidateModel();
            // Refresh the conference status only if attached
            MapStringString confDetails = CallManager::instance().getConferenceDetails(linked.owner.id, confId);
            if (confDetails["STATE"] == "ACTIVE_ATTACHED")
                linked.selectConversation(conversation.uid);
            return;
        }
    }
}

void
ConversationModelPimpl::updateInteractionStatus(const QString& accountId,
                                                const QString& conversationId,
                                                const QString& peerUri,
                                                const QString& messageId,
                                                int status)
{
    if (accountId != linked.owner.id) {
        return;
    }
    try {
        auto& conversation = getConversationForUid(conversationId).get();
        // Proceed only if the conversation is a swarm
        if (!conversation.isSwarm() || peerUri == linked.owner.profileInfo.uri) {
            return;
        }

        auto emitDisplayed = false;
        using namespace libjami::Account;
        auto msgState = static_cast<MessageStates>(status);
        auto& interactions = conversation.interactions;
        interactions->with(messageId, [&](const QString& id, const interaction::Info&) {
            interaction::Status newState;
            if (msgState == MessageStates::SENDING) {
                newState = interaction::Status::SENDING;
            } else if (msgState == MessageStates::SENT) {
                newState = interaction::Status::SUCCESS;
            } else if (msgState == MessageStates::DISPLAYED) {
                newState = interaction::Status::DISPLAYED;
            } else {
                return;
            }
            if (interactions->updateStatus(id, newState) && newState == interaction::Status::DISPLAYED) {
                emitDisplayed = true;
            }
        });

        if (emitDisplayed)
            conversation.interactions->setRead(peerUri, messageId);
    } catch (const std::out_of_range& e) {
        qDebug() << "Cannot update message status for conversation that does not exist.";
    }
}

void
ConversationModelPimpl::slotConferenceRemoved(const QString& accountId, const QString& confId)
{
    if (accountId != linked.owner.id)
        return;
    // Get conversation
    for (auto& i : conversations) {
        if (i.confId == confId) {
            i.confId = "";
        }
    }
}

void
ConversationModelPimpl::slotComposingStatusChanged(const QString& accountId,
                                                   const QString& convId,
                                                   const QString& contactUri,
                                                   bool isComposing)
{
    if (accountId != linked.owner.id)
        return;

    try {
        auto& conversation = getConversationForUid(convId).get();
        if (isComposing)
            conversation.typers.insert(contactUri);
        else
            conversation.typers.remove(contactUri);
    } catch (const std::out_of_range& e) {
        qDebug() << "Cannot update message status for conversation that does not exist.";
    }

    Q_EMIT linked.composingStatusChanged(convId, contactUri, isComposing);
}

int
ConversationModelPimpl::getNumberOfUnreadMessagesFor(const QString& uid)
{
    return storage::countUnreadFromInteractions(db, uid);
}

void
ConversationModel::setIsComposing(const QString& convUid, bool isComposing)
{
    try {
        auto& conversation = pimpl_->getConversationForUid(convUid).get();
        QString to = conversation.mode != conversation::Mode::NON_SWARM
                         ? "swarm:" + convUid
                         : "jami:" + pimpl_->peersForConversation(conversation).front();
        ConfigurationManager::instance().setIsComposing(owner.id, to, isComposing);
    } catch (...) {
    }
}

void
ConversationModel::sendFile(const QString& convUid, const QString& path, const QString& filename, const QString& parent)
{
    try {
        auto& conversation = pimpl_->getConversationForUid(convUid, true).get();
        if (conversation.isSwarm()) {
            owner.dataTransferModel->sendFile(owner.id, convUid, path, filename, parent);
            return;
        }
        auto peers = pimpl_->peersForConversation(conversation);
        if (peers.size() < 1) {
            qDebug() << "Send file error: cannot send file in conversation with no participants.";
            return;
        }
        /* isTemporary, and conversationReady callback used only for non-swarm conversation,
         because for swarm, conversation already configured at this point.
         Conversations for new contact from search result are NON_SWARM but after receiving
         conversationReady callback could be updated to ONE_TO_ONE. We still use conversationReady
         callback for one_to_one conversation to check if contact is blocked*/
        const auto peerId = peers.front();
        bool isTemporary = peerId == convUid || convUid == "SEARCHSIP";

        /* It is necessary to make a copy of convUid since it may very well point to
         a field in the temporary conversation, which is going to be destroyed by
         slotContactAdded() (indirectly triggered by sendContactrequest(). Not doing
         so may result in use after free/crash. */
        auto convUidCopy = convUid;

        pimpl_->sendContactRequest(peerId);

        auto cb = ([this, peerId, path, filename, parent](QString conversationId) {
            try {
                auto conversationOpt = getConversationForUid(conversationId);
                if (!conversationOpt.has_value()) {
                    qDebug() << "Cannot send file.";
                    return;
                }
                auto contactInfo = owner.contactModel->getContact(peerId);
                if (contactInfo.isBanned) {
                    qDebug() << "ContactModel::sendFile: denied, contact is banned.";
                    return;
                }
                owner.dataTransferModel->sendFile(owner.id, conversationId, path, filename, parent);
            } catch (...) {
            }
        });

        if (isTemporary) {
            QMetaObject::Connection* const connection = new QMetaObject::Connection;
            *connection = connect(this,
                                  &ConversationModel::conversationReady,
                                  [cb, connection, convUidCopy](QString conversationId, QString participantId) {
                                      if (participantId != convUidCopy) {
                                          return;
                                      }
                                      cb(conversationId);
                                      QObject::disconnect(*connection);
                                      if (connection) {
                                          delete connection;
                                      }
                                  });
        } else {
            cb(convUidCopy);
        }
    } catch (const std::out_of_range& e) {
        qDebug() << "Cannot send file to conversation that does not exist.";
    }
}

void
ConversationModel::getConvMediasInfos(const QString& accountId,
                                      const QString& conversationId,
                                      const QString& text,
                                      bool isMedia)
{
    if (isMedia)
        pimpl_->mediaResearchRequestId = ConfigurationManager::instance().searchConversation(
            accountId, conversationId, "", "", text, "application/data-transfer+json", 0, 0, 0, 0);
    else
        pimpl_->msgResearchRequestId = ConfigurationManager::instance().searchConversation(
            accountId, conversationId, "", "", text, "text/plain", 0, 0, 0, 0);
}

void
ConversationModel::acceptTransfer(const QString& convUid, const QString& interactionId)
{
    lrc::api::datatransfer::Info info = {};
    getTransferInfo(convUid, interactionId, info);
    pimpl_->acceptTransfer(convUid, interactionId);
}

void
ConversationModel::cancelTransfer(const QString& convUid, const QString& fileId)
{
    // For this action, we change interaction status before effective canceling as daemon will
    // emit Finished event code immediately (before leaving this method) in non-DBus mode.
    auto conversationIdx = pimpl_->indexOf(convUid);
    bool emitUpdated = false;
    if (conversationIdx != -1) {
        auto& interactions = pimpl_->conversations[conversationIdx].interactions;
        if (interactions->updateTransferStatus(fileId, interaction::TransferStatus::TRANSFER_CANCELED)) {
            // update information in the db
            storage::updateInteractionTransferStatus(pimpl_->db, fileId, interaction::TransferStatus::TRANSFER_CANCELED);
            emitUpdated = true;
        }
    }
    if (emitUpdated) {
        // for swarm conversations we need to provide conversation id to accept file, for not swarm
        // conversations we need peer uri
        lrc::api::datatransfer::Info info = {};
        getTransferInfo(convUid, fileId, info);
        // Forward cancel action to daemon (will invoke slotTransferStatusCanceled)
        owner.dataTransferModel->cancel(owner.id, convUid, fileId);
        pimpl_->invalidateModel();
        Q_EMIT pimpl_->behaviorController.newReadInteraction(owner.id, convUid, fileId);
    }
}

void
ConversationModel::getTransferInfo(const QString& conversationId,
                                   const QString& interactionId,
                                   datatransfer::Info& info) const
{
    auto convOpt = getConversationForUid(conversationId);
    if (!convOpt)
        return;
    auto fileId = owner.dataTransferModel->getFileIdFromInteractionId(interactionId);
    if (convOpt->get().mode == conversation::Mode::NON_SWARM) {
        return;
    } else {
        QString path;
        qlonglong bytesProgress = 0, totalSize = 0;
        owner.dataTransferModel->fileTransferInfo(owner.id, conversationId, fileId, path, totalSize, bytesProgress);
        info.path = path;
        info.totalSize = totalSize;
        info.progress = bytesProgress;
    }
}

void
ConversationModel::removeFile(const QString& conversationId, const QString& interactionId, const QString& path)
{
    auto convOpt = getConversationForUid(conversationId);
    if (!convOpt)
        return;

    QFile::remove(path);
    convOpt->get().interactions->updateTransferStatus(interactionId, interaction::TransferStatus::TRANSFER_CANCELED);
}

int
ConversationModel::getNumberOfUnreadMessagesFor(const QString& convUid)
{
    return pimpl_->getNumberOfUnreadMessagesFor(convUid);
}

bool
ConversationModelPimpl::usefulDataFromDataTransfer(const QString& fileId,
                                                   const datatransfer::Info& info,
                                                   QString& interactionId,
                                                   QString& conversationId)
{
    if (info.accountId != linked.owner.id)
        return false;
    try {
        interactionId = linked.owner.dataTransferModel->getInteractionIdFromFileId(fileId);
        conversationId = info.conversationId.isEmpty() ? storage::conversationIdFromInteractionId(db, interactionId)
                                                       : info.conversationId;
    } catch (const std::out_of_range& e) {
        qWarning() << "Couldn't get interaction from daemon Id: " << fileId;
        return false;
    }
    return true;
}

void
ConversationModelPimpl::slotTransferStatusCreated(const QString& fileId, datatransfer::Info info)
{
    auto isSip = linked.owner.profileInfo.type == profile::Type::SIP;
    // check if transfer is for the current account
    if (info.accountId != linked.owner.id)
        return;

    const MapStringString accountDetails = ConfigurationManager::instance().getAccountDetails(linked.owner.id);
    if (accountDetails.empty())
        return;
    // create a new conversation if needed
    auto convIds = storage::getConversationsWithPeer(db, info.peerUri);
    bool isRequest = false;
    if (convIds.empty()) {
        // in case if we receive file after removing contact add conversation request. If we have
        // swarm request this function will do nothing.
        try {
            auto contact = linked.owner.contactModel->getContact(info.peerUri);
            isRequest = contact.profileInfo.type == profile::Type::PENDING;
            if (isRequest && !contact.isBanned && info.peerUri != linked.owner.profileInfo.uri) {
                addContactRequest(info.peerUri);
                if (isSip) {
                    convIds.push_back(storage::beginConversationWithPeer(db, contact.profileInfo.uri));
                    auto& conv = getConversationForPeerUri(contact.profileInfo.uri).get();
                    conv.uid = convIds[0];
                }
            } else {
                return;
            }
        } catch (const std::out_of_range&) {
            return;
        }
    }

    // add interaction to the db
    const auto& convId = convIds[0];
    auto interactionId = storage::addDataTransferToConversation(db, convId, info);

    // map fileId and interactionId for latter retrivial from client (that only known the interactionId)
    linked.owner.dataTransferModel->registerTransferId(fileId, interactionId);

    auto interaction = interaction::Info {info.isOutgoing ? "" : info.peerUri,
                                          info.isOutgoing ? info.path : info.displayName,
                                          std::time(nullptr),
                                          0,
                                          interaction::Type::DATA_TRANSFER,
                                          interaction::Status::SENDING,
                                          false,
                                          interaction::TransferStatus::TRANSFER_CREATED};

    // prepare interaction Info and emit signal for the client
    auto conversationIdx = indexOf(convId);
    if (conversationIdx == -1) {
        addConversationWith(convId, info.peerUri, isRequest);
        Q_EMIT linked.newConversation(convId);
    } else {
        conversations[conversationIdx].interactions->append(interactionId, interaction);
        conversations[conversationIdx].unreadMessages = getNumberOfUnreadMessagesFor(convId);
    }
    Q_EMIT behaviorController.newUnreadInteraction(linked.owner.id, convId, interactionId, interaction);
    Q_EMIT linked.newInteraction(convId, interactionId, interaction);

    invalidateModel();
    Q_EMIT linked.modelChanged();
    Q_EMIT linked.dataChanged(conversationIdx);
}

void
ConversationModelPimpl::slotTransferStatusAwaitingPeer(const QString& fileId, datatransfer::Info info)
{
    if (info.accountId != linked.owner.id)
        return;
    bool intUpdated;
    updateTransferStatus(fileId, info, interaction::TransferStatus::TRANSFER_AWAITING_PEER, intUpdated);
}

void
ConversationModelPimpl::slotTransferStatusAwaitingHost(const QString& fileId, datatransfer::Info info)
{
    if (info.accountId != linked.owner.id)
        return;
    awaitingHost(fileId, info);
}

bool
ConversationModelPimpl::hasOneOneSwarmWith(const contact::Info& participant)
{
    try {
        if (!participant.conversationId.isEmpty()) {
            auto& conversation = getConversationForUid(participant.conversationId).get();
            return conversation.mode == conversation::Mode::ONE_TO_ONE;
        }
    } catch (std::out_of_range&) {
    }
    return false;
}

void
ConversationModelPimpl::awaitingHost(const QString& fileId, datatransfer::Info info)
{
    if (info.accountId != linked.owner.id)
        return;
    QString interactionId;
    QString conversationId;
    if (not usefulDataFromDataTransfer(fileId, info, interactionId, conversationId))
        return;

    bool intUpdated;

    if (!updateTransferStatus(fileId, info, interaction::TransferStatus::TRANSFER_AWAITING_HOST, intUpdated)) {
        return;
    }
    if (!intUpdated) {
        return;
    }
    auto conversationIdx = indexOf(conversationId);
    auto& peers = peersForConversation(conversations[conversationIdx]);
    handleIncomingFile(conversationId, interactionId, info.totalSize);
}

void
ConversationModelPimpl::handleIncomingFile(const QString& convId, const QString& interactionId, int totalSize)
{
    // If it's an accepted file type and less than 20 MB, accept transfer.
    if (linked.owner.accountModel->autoTransferFromTrusted) {
        if (linked.owner.accountModel->autoTransferSizeThreshold == 0
            || (totalSize > 0
                && static_cast<unsigned>(totalSize)
                       < linked.owner.accountModel->autoTransferSizeThreshold * 1024 * 1024)) {
            acceptTransfer(convId, interactionId);
        }
    }
}

void
ConversationModelPimpl::acceptTransfer(const QString& convUid, const QString& interactionId)
{
    auto& conversation = getConversationForUid(convUid).get();
    if (conversation.isLegacy()) // Ignore legacy
        return;

    auto& interactions = conversation.interactions;
    if (!interactions->with(interactionId, [&](const QString&, interaction::Info& interaction) {
            auto fileId = interaction.commit["fileId"];
            if (fileId.isEmpty()) {
                qWarning() << "Cannot download file without fileId";
                return;
            }
            linked.owner.dataTransferModel->download(linked.owner.id, convUid, interactionId, fileId);
        })) {
        qWarning() << "Cannot download file without valid interaction";
    }
}

void
ConversationModelPimpl::invalidateModel()
{
    filteredConversations.invalidate();
    customFilteredConversations.invalidate();
}

void
ConversationModelPimpl::emplaceBackConversation(conversation::Info&& conversation)
{
    if (conversationMap.find(conversation.uid) != conversationMap.end())
        return;
    Q_EMIT linked.beginInsertRows(conversations.size());
    conversations.emplace_back(std::move(conversation));
    auto newIndex = static_cast<int>(conversations.size() - 1);
    auto& newConv = conversations.back();
    conversationMap.emplace(newConv.uid, newIndex);
    Q_EMIT linked.endInsertRows();
}

void
ConversationModelPimpl::eraseConversation(const QString& convId)
{
    auto it = conversationMap.find(convId);
    if (it == conversationMap.end())
        return;
    auto index = it->second;
    conversationMap.erase(it);
    eraseConversation(index);
}

void
ConversationModelPimpl::eraseConversation(int index)
{
    if (index < 0 || index >= static_cast<int>(conversations.size()))
        return;
    auto uid = conversations.at(index).uid;
    conversationMap.erase(uid);
    Q_EMIT linked.beginRemoveRows(index);
    conversations.erase(conversations.begin() + index);
    for (auto& entry : conversationMap) {
        if (entry.second > index) {
            entry.second--;
        }
    }
    Q_EMIT linked.endRemoveRows();
}

void
ConversationModelPimpl::slotTransferStatusOngoing(const QString& fileId, datatransfer::Info info)
{
    if (info.accountId != linked.owner.id)
        return;
    QString interactionId;
    QString conversationId;
    if (not usefulDataFromDataTransfer(fileId, info, interactionId, conversationId))
        return;
    bool intUpdated;

    if (!updateTransferStatus(fileId, info, interaction::TransferStatus::TRANSFER_ONGOING, intUpdated)) {
        return;
    }
    if (!intUpdated) {
        return;
    }
    auto conversationIdx = indexOf(conversationId);
    auto* timer = new QTimer();
    connect(timer, &QTimer::timeout, this, [=] { updateTransferProgress(timer, conversationIdx, interactionId); });
    timer->start(1000);
}

void
ConversationModelPimpl::slotTransferStatusFinished(const QString& fileId, datatransfer::Info info)
{
    if (info.accountId != linked.owner.id)
        return;
    QString interactionId;
    QString conversationId;
    if (not usefulDataFromDataTransfer(fileId, info, interactionId, conversationId))
        return;
    // prepare interaction Info and emit signal for the client
    auto conversationIdx = indexOf(conversationId);
    if (conversationIdx != -1) {
        bool emitUpdated = false;
        auto newStatus = interaction::TransferStatus::TRANSFER_FINISHED;
        auto& interactions = conversations[conversationIdx].interactions;
        interactions->with(interactionId, [&](const QString& id, interaction::Info& interaction) {
            // We need to check if current status is ONGOING as CANCELED must not be
            // transformed into FINISHED
            if (interaction.transferStatus == interaction::TransferStatus::TRANSFER_ONGOING) {
                emitUpdated = true;
                interactions->updateTransferStatus(id, newStatus);
            }
        });
        if (emitUpdated) {
            invalidateModel();
            if (conversations[conversationIdx].mode != conversation::Mode::NON_SWARM) {
                if (transfIdToDbIntId.find(fileId) != transfIdToDbIntId.end()) {
                    auto dbIntId = transfIdToDbIntId[fileId];
                    storage::updateInteractionTransferStatus(db, dbIntId, newStatus);
                }
            } else {
                storage::updateInteractionTransferStatus(db, interactionId, newStatus);
            }
            transfIdToDbIntId.remove(fileId);
        }
    }
}

void
ConversationModelPimpl::slotTransferStatusCanceled(const QString& fileId, datatransfer::Info info)
{
    if (info.accountId != linked.owner.id)
        return;
    bool intUpdated;
    updateTransferStatus(fileId, info, interaction::TransferStatus::TRANSFER_CANCELED, intUpdated);
}

void
ConversationModelPimpl::slotTransferStatusError(const QString& fileId, datatransfer::Info info)
{
    if (info.accountId != linked.owner.id)
        return;
    bool intUpdated;
    updateTransferStatus(fileId, info, interaction::TransferStatus::TRANSFER_ERROR, intUpdated);
}

void
ConversationModelPimpl::slotTransferStatusUnjoinable(const QString& fileId, datatransfer::Info info)
{
    if (info.accountId != linked.owner.id)
        return;
    bool intUpdated;
    updateTransferStatus(fileId, info, interaction::TransferStatus::TRANSFER_UNJOINABLE_PEER, intUpdated);
}

void
ConversationModelPimpl::slotTransferStatusTimeoutExpired(const QString& fileId, datatransfer::Info info)
{
    if (info.accountId != linked.owner.id)
        return;
    bool intUpdated;
    updateTransferStatus(fileId, info, interaction::TransferStatus::TRANSFER_TIMEOUT_EXPIRED, intUpdated);
}

bool
ConversationModelPimpl::updateTransferStatus(const QString& fileId,
                                             datatransfer::Info info,
                                             interaction::TransferStatus newStatus,
                                             bool& updated)
{
    QString interactionId;
    QString conversationId;
    if (not usefulDataFromDataTransfer(fileId, info, interactionId, conversationId)) {
        return false;
    }

    auto conversationIdx = indexOf(conversationId);
    if (conversationIdx < 0) {
        return false;
    }
    auto& conversation = conversations[conversationIdx];
    if (conversation.isLegacy()) {
        storage::updateInteractionTransferStatus(db, interactionId, newStatus);
    }
    auto& interactions = conversation.interactions;
    bool emitUpdated = interactions->updateTransferStatus(interactionId,
                                                          newStatus,
                                                          conversation.isSwarm() ? info.path : QString());
    if (emitUpdated) {
        invalidateModel();
    }
    updated = emitUpdated;
    return true;
}

void
ConversationModelPimpl::updateTransferProgress(QTimer* timer, int conversationIdx, const QString& interactionId)
{
    try {
        bool emitUpdated = false;
        {
            const auto& interactions = conversations[conversationIdx].interactions;
            interactions->with(interactionId, [&](const QString& id, interaction::Info& interaction) {
                if (interaction.transferStatus == interaction::TransferStatus::TRANSFER_ONGOING) {
                    emitUpdated = true;
                    interactions->updateTransferStatus(id, interaction::TransferStatus::TRANSFER_ONGOING);
                }
            });
        }
        if (emitUpdated)
            return;
    } catch (...) {
    }

    timer->stop();
    timer->deleteLater();
}

void
ConversationModelPimpl::slotConversationPreferencesUpdated(const QString&,
                                                           const QString& conversationId,
                                                           const MapStringString& preferences)
{
    try {
        auto& conversation = getConversationForUid(conversationId).get();
        conversation.preferences = preferences;
        Q_EMIT linked.conversationPreferencesUpdated(conversationId);
    } catch (const std::out_of_range&) {
        qWarning() << Q_FUNC_INFO << "Unable to find conversation" << conversationId;
    }
}

} // namespace lrc

#include "api/moc_conversationmodel.cpp"
#include "conversationmodel.moc"
