/*
 * Copyright (C) 2017-2026 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/callmodel.h"

// Lrc
#include "callbackshandler.h"
#include "api/avmodel.h"
#include "api/behaviorcontroller.h"
#include "api/conversationmodel.h"
#include "api/codecmodel.h"

#include "api/contact.h"
#include "api/contactmodel.h"
#include "api/pluginmodel.h"
#include "api/callparticipantsmodel.h"
#include "api/lrc.h"
#include "api/accountmodel.h"
#include "authority/storagehelper.h"
#include "dbus/callmanager.h"
#include "dbus/videomanager.h"
#include "vcard.h"
#include "renderer.h"
#include "typedefs.h"
#include "uri.h"

// Ring daemon
#include <media_const.h>
#include <account_const.h>

// Qt
#include <QObject>
#include <QString>
#include <QUrl>
#include <QSize>
#include <qlogging.h>
#include <qtcoreexports.h>

// std
#include <chrono>
#include <random>
#include <map>

#ifdef WIN32
#define NOMINMAX
#include "Windows.h"
#endif

using namespace libjami::Media;

constexpr static const char HARDWARE_ACCELERATION[] = "HARDWARE_ACCELERATION";
constexpr static const char AUDIO_CODEC[] = "AUDIO_CODEC";
constexpr static const char CALL_ID[] = "CALL_ID";

static std::uniform_int_distribution<int> dis {0, std::numeric_limits<int>::max()};
static const std::map<short, QString>

    sip_call_status_code_map {{0, QObject::tr("Null")},
                              {100, QObject::tr("Trying")},
                              {180, QObject::tr("Ringing")},
                              {181, QObject::tr("Being Forwarded")},
                              {182, QObject::tr("Queued")},
                              {183, QObject::tr("Progress")},
                              {200, QObject::tr("OK")},
                              {202, QObject::tr("Accepted")},
                              {300, QObject::tr("Multiple Choices")},
                              {301, QObject::tr("Moved Permanently")},
                              {302, QObject::tr("Moved Temporarily")},
                              {305, QObject::tr("Use Proxy")},
                              {380, QObject::tr("Alternative Service")},
                              {400, QObject::tr("Bad Request")},
                              {401, QObject::tr("Unauthorized")},
                              {402, QObject::tr("Payment Required")},
                              {403, QObject::tr("Forbidden")},
                              {404, QObject::tr("Not Found")},
                              {405, QObject::tr("Method Not Allowed")},
                              {406, QObject::tr("Not Acceptable")},
                              {407, QObject::tr("Proxy Authentication Required")},
                              {408, QObject::tr("Request Timeout")},
                              {410, QObject::tr("Gone")},
                              {413, QObject::tr("Request Entity Too Large")},
                              {414, QObject::tr("Request URI Too Long")},
                              {415, QObject::tr("Unsupported Media Type")},
                              {416, QObject::tr("Unsupported URI Scheme")},
                              {420, QObject::tr("Bad Extension")},
                              {421, QObject::tr("Extension Required")},
                              {422, QObject::tr("Session Timer Too Small")},
                              {423, QObject::tr("Interval Too Brief")},
                              {480, QObject::tr("Temporarily Unavailable")},
                              {481, QObject::tr("Call TSX Does Not Exist")},
                              {482, QObject::tr("Loop Detected")},
                              {483, QObject::tr("Too Many Hops")},
                              {484, QObject::tr("Address Incomplete")},
                              {485, QObject::tr("Ambiguous")},
                              {486, QObject::tr("Busy")},
                              {487, QObject::tr("Request Terminated")},
                              {488, QObject::tr("Not Acceptable")},
                              {489, QObject::tr("Bad Event")},
                              {490, QObject::tr("Request Updated")},
                              {491, QObject::tr("Request Pending")},
                              {493, QObject::tr("Undecipherable")},
                              {500, QObject::tr("Internal Server Error")},
                              {501, QObject::tr("Not Implemented")},
                              {502, QObject::tr("Bad Gateway")},
                              {503, QObject::tr("Service Unavailable")},
                              {504, QObject::tr("Server Timeout")},
                              {505, QObject::tr("Version Not Supported")},
                              {513, QObject::tr("Message Too Large")},
                              {580, QObject::tr("Precondition Failure")},
                              {600, QObject::tr("Busy Everywhere")},
                              {603, QObject::tr("Call Declined")},
                              {604, QObject::tr("Does Not Exist Anywhere")},
                              {606, QObject::tr("Not Acceptable Anywhere")}};

namespace lrc {

using namespace api;

class CallModelPimpl : public QObject
{
    Q_OBJECT
public:
    CallModelPimpl(const CallModel& linked,
                   Lrc& lrc,
                   const CallbacksHandler& callbacksHandler,
                   const BehaviorController& behaviorController);
    ~CallModelPimpl();

    QVariantList callAdvancedInformation();
    MapStringString advancedInformationForCallId(QString callId);

    /**
     * Get a suitable fallback conversation for when the client should select a new one.
     * For example if the peer whose conversation was used to host the conference leaves the conference,
     * we need to pick another conversation to display the conference call.
     * @param confId the conference id
     * @return the conversation to use as fallback, nullopt if client does not need to change conversation or if none found
     */
    OptRef<conversation::Info> getFallbackConversationForConference(const QString& confId);

    QStringList getCallIds();

    CallModel::CallInfoMap calls;
    CallModel::CallParticipantsModelMap participantsModel;
    const CallbacksHandler& callbacksHandler;
    const CallModel& linked;
    const BehaviorController& behaviorController;

    /**
     * key = peer's uri
     * vector = chunks
     * @note chunks are counted from 1 to number of parts. We use 0 to store the actual number of
     * parts stored
     */
    std::map<QString, VectorString> vcardsChunks;

    /**
     * Retrieve active calls from the daemon and init the model
     */
    void initCallFromDaemon();

    /**
     * Retrieve active conferences from the daemon and init the model
     */
    void initConferencesFromDaemon();

    /**
     * Check if media device is muted
     */
    bool checkMediaDeviceMuted(const MapStringString& mediaAttributes);

    bool manageCurrentCall_ {true};
    QString currentCall_ {};

    Lrc& lrc;

    QList<call::PendingConferenceeInfo> pendingConferencees_;
    QString waitForConference_ {};

private:
    // Tracks last known participant count per conference to detect size changes in slotOnConferenceInfosUpdated.
    std::map<QString, size_t> previousCallListSizes_;

    void applyFallbackConversationSwitch(const QString& confId,
                                         OptRef<conversation::Info> currentConversation,
                                         OptRef<conversation::Info> fallbackConversation,
                                         const QString& newCallId,
                                         const QString& newConfId);

public Q_SLOTS:
    /**
     * Connect this signal to know when a call arrives
     * @param accountId the one who receives the call
     * @param callId the call id
     * @param mediaList new media received
     */
    void slotMediaChangeRequested(const QString& accountId,
                                  const QString& callId,
                                  const VectorMapStringString& mediaList);
    /**
     * Listen from CallbacksHandler when a call arrives
     * @param accountId
     * @param callId
     * @param from
     * @param mediaList
     */
    void slotIncomingCall(const QString& accountId,
                          const QString& callId,
                          const QString& from,
                          const VectorMapStringString& mediaList);
    /**
     * Listen from CallbacksHandler when a call got a new state
     * @param accountId
     * @param callId
     * @param state the new state
     * @param code unused
     */
    void slotCallStateChanged(const QString& accountId, const QString& callId, const QString& state, int code);
    /**
     * Listen from CallbacksHandler when a call medias are ready
     * @param callId
     * @param event
     * @param mediaList
     */
    void slotMediaNegotiationStatus(const QString& callId, const QString& event, const VectorMapStringString& mediaList);
    /**
     * Listen from CallbacksHandler when a VCard chunk is incoming
     * @param accountId
     * @param callId
     * @param from
     * @param part
     * @param numberOfParts
     * @param payload
     */
    void slotincomingVCardChunk(const QString& accountId,
                                const QString& callId,
                                const QString& from,
                                int part,
                                int numberOfParts,
                                const QString& payload);
    /**
     * Listen from CallbacksHandler when a conference is created.
     * @param callId
     */
    void slotConferenceCreated(const QString& accountId, const QString& conversationId, const QString& callId);
    void slotConferenceChanged(const QString& accountId, const QString& callId, const QString& state);
    void slotConferenceRemoved(const QString& accountId, const QString& confId);
    /**
     * Listen from CallbacksHandler when a voice mail notice is incoming
     * @param accountId
     * @param newCount
     * @param oldCount
     * @param urgentCount
     */
    void slotVoiceMailNotify(const QString& accountId, int newCount, int oldCount, int urgentCount);
    /**
     * Listen from CallManager when a conference layout is updated
     * @param confId
     * @param infos
     */
    void slotOnConferenceInfosUpdated(const QString& confId, const VectorMapStringString& infos);
    /**
     * Listen from CallbacksHandler when the peer start recording
     * @param callId
     * @param peerUri
     * @param state the new state
     */
    void onRemoteRecordingChanged(const QString& callId, const QString& peerUri, bool state);
    /**
     * Listen from CallbacksHandler when we start/stop recording
     * @param callId
     * @param state the new state
     */
    void onRecordingStateChanged(const QString& callId, bool state);
};

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

CallModel::~CallModel() {}

const call::Info&
CallModel::getCallFromURI(const QString& uri, bool notOver) const
{
    // For a non-SIP account the scheme can be ring:. Sometimes it can miss, and will be certainly
    // replaced by jami://.
    // Just make the comparaison ignoring the scheme and check the rest.
    auto uriObj = URI(uri);
    for (const auto& call : pimpl_->calls) {
        auto contactUri = URI(call.second->peerUri);
        if (uriObj.userinfo() == contactUri.userinfo() and uriObj.hostname() == contactUri.hostname()) {
            if (!notOver || !call::isTerminating(call.second->status))
                return *call.second;
        }
    }
    throw std::out_of_range("No call at URI " + uri.toStdString());
}

const call::Info&
CallModel::getConferenceFromURI(const QString& uri) const
{
    for (const auto& call : pimpl_->calls) {
        if (call.second->type == call::Type::CONFERENCE) {
            QStringList callList = CallManager::instance().getParticipantList(owner.id, call.first);
            Q_FOREACH (const auto& callId, callList) {
                try {
                    if (pimpl_->calls.find(callId) != pimpl_->calls.end() && pimpl_->calls[callId]->peerUri == uri) {
                        return *call.second;
                    }
                } catch (...) {
                }
            }
        }
    }
    throw std::out_of_range("No call at URI " + uri.toStdString());
}

VectorString
CallModel::getConferenceSubcalls(const QString& confId)
{
    QStringList callList = CallManager::instance().getParticipantList(owner.id, confId);
    VectorString result;
    result.reserve(callList.size());
    Q_FOREACH (const auto& callId, callList) {
        result.push_back(callId);
    }
    return result;
}

const call::Info&
CallModel::getCall(const QString& uid) const
{
    return *pimpl_->calls.at(uid);
}

const CallParticipants&
CallModel::getParticipantsInfos(const QString& callId)
{
    if (pimpl_->participantsModel.find(callId) == pimpl_->participantsModel.end()) {
        VectorMapStringString infos = {};
        pimpl_->participantsModel.emplace(callId, std::make_shared<CallParticipants>(infos, callId, pimpl_->linked));
    }
    return *pimpl_->participantsModel.at(callId);
}

void
CallModel::setVideoMuted(const QString& callId, bool videoMuted)
{
    auto call = pimpl_->calls.find(callId);
    if (call == pimpl_->calls.end())
        return;

    auto& callInfo = call->second;
    callInfo->videoMuted = videoMuted;

    for (auto& media : callInfo->mediaList) {
        if (!media.contains(MediaAttributeKey::MEDIA_TYPE))
            continue;

        if (media[MediaAttributeKey::MEDIA_TYPE] == MediaAttributeValue::VIDEO) {
            media[MediaAttributeKey::MUTED] = videoMuted ? TRUE_STR : FALSE_STR;
        }
    }
}

void
initializeMediaList(VectorMapStringString& mediaList, bool audioOnly, bool videoMuted)
{
    mediaList.push_back({{MediaAttributeKey::MEDIA_TYPE, MediaAttributeValue::AUDIO},
                         {MediaAttributeKey::ENABLED, TRUE_STR},
                         {MediaAttributeKey::MUTED, FALSE_STR},
                         {MediaAttributeKey::SOURCE, ""},
                         {MediaAttributeKey::LABEL, "audio_0"}});
    if (audioOnly)
        return;
    if (videoMuted) {
        mediaList.push_back({{MediaAttributeKey::MEDIA_TYPE, MediaAttributeValue::VIDEO},
                             {MediaAttributeKey::ENABLED, TRUE_STR},
                             {MediaAttributeKey::MUTED, TRUE_STR},
                             {MediaAttributeKey::SOURCE, ""},
                             {MediaAttributeKey::LABEL, "video_0"}});
        return;
    }

    const auto defaultCamera = VideoManager::instance().getDefaultDevice();
    QString cameraMRL = "camera://" + defaultCamera;
    mediaList.push_back({{MediaAttributeKey::MEDIA_TYPE, MediaAttributeValue::VIDEO},
                         {MediaAttributeKey::ENABLED, TRUE_STR},
                         {MediaAttributeKey::MUTED, FALSE_STR},
                         {MediaAttributeKey::SOURCE, cameraMRL},
                         {MediaAttributeKey::LABEL, "video_0"}});
    return;
}

QString
CallModel::createCall(const QString& uri, bool isAudioOnly, VectorMapStringString mediaList, bool videoMuted)
{
    bool isSwarm = uri.startsWith("swarm:") || uri.startsWith("rdv:");
    if (mediaList.isEmpty()) {
        initializeMediaList(mediaList, isAudioOnly, videoMuted && isSwarm);
    }
#ifdef ENABLE_LIBWRAP
    auto callId = CallManager::instance().placeCallWithMedia(owner.id, uri, mediaList);
#else  // dbus
    // do not use auto here (QDBusPendingReply<QString>)
    QString callId = CallManager::instance().placeCallWithMedia(owner.id, uri, mediaList);
#endif // ENABLE_LIBWRAP

    if (callId.isEmpty()) {
        if (isSwarm) {
            pimpl_->waitForConference_ = uri;
            return {};
        }
        qWarning() << "No call placed between (account: " << owner.id << ", contact: " << uri << ")";
        return "";
    }

    auto callInfo = std::make_shared<call::Info>();
    callInfo->id = callId;
    callInfo->peerUri = uri;
    callInfo->isOutgoing = true;
    callInfo->status = call::Status::SEARCHING;
    callInfo->type = call::Type::DIALOG;
    callInfo->isAudioOnly = isAudioOnly;
    callInfo->videoMuted = videoMuted;
    callInfo->mediaList = mediaList;
    pimpl_->calls.emplace(callId, std::move(callInfo));

    return callId;
}

QList<QVariant>
CallModel::getAdvancedInformation() const
{
    return pimpl_->callAdvancedInformation();
}

MapStringString
CallModel::advancedInformationForCallId(QString callId) const
{
    return pimpl_->advancedInformationForCallId(callId);
}

QStringList
CallModel::getCallIds() const
{
    return pimpl_->getCallIds();
}

void
CallModel::emplaceConversationConference(const QString& confId)
{
    if (hasCall(confId))
        return;

    auto callInfo = std::make_shared<call::Info>();
    callInfo->id = confId;
    callInfo->isOutgoing = false;
    callInfo->status = call::Status::SEARCHING;
    callInfo->type = call::Type::CONFERENCE;
    callInfo->isAudioOnly = false;
    callInfo->videoMuted = false;
    callInfo->mediaList = {};
    pimpl_->calls.emplace(confId, std::move(callInfo));
}

void
CallModel::muteMedia(const QString& callId, const QString& label, bool mute)
{
    auto& callInfo = pimpl_->calls[callId];
    if (!callInfo)
        return;

    auto proposedList = callInfo->mediaList;
    if (proposedList.isEmpty())
        return;
    for (auto& media : proposedList)
        if (media[MediaAttributeKey::LABEL] == label)
            media[MediaAttributeKey::MUTED] = mute ? TRUE_STR : FALSE_STR;
    CallManager::instance().requestMediaChange(owner.id, callId, proposedList);
}

void
CallModel::replaceDefaultCamera(const QString& callId, const QString& deviceId)
{
    auto& callInfo = pimpl_->calls[callId];
    if (!callInfo)
        return;

    VectorMapStringString proposedList = callInfo->mediaList;
    QString oldPreview, newPreview;
    for (auto& media : proposedList) {
        if (media[MediaAttributeKey::MEDIA_TYPE] == MediaAttributeValue::VIDEO
            && media[MediaAttributeKey::SOURCE].startsWith(libjami::Media::VideoProtocolPrefix::CAMERA)) {
            oldPreview = media[MediaAttributeKey::SOURCE];
            QString resource = QString("%1%2%3")
                                   .arg(libjami::Media::VideoProtocolPrefix::CAMERA)
                                   .arg(libjami::Media::VideoProtocolPrefix::SEPARATOR)
                                   .arg(deviceId);
            media[MediaAttributeKey::SOURCE] = resource;
            newPreview = resource;
            break;
        }
    }

    if (!newPreview.isEmpty()) {
        pimpl_->lrc.getAVModel().stopPreview(oldPreview);
        pimpl_->lrc.getAVModel().startPreview(newPreview);
    }

    CallManager::instance().requestMediaChange(owner.id, callId, proposedList);
}

VectorMapStringString
CallModel::getProposed(VectorMapStringString mediaList,
                       const QString& callId,
                       const QString& source,
                       MediaRequestType type,
                       bool mute,
                       bool shareAudio)
{
    QString resource {};
    auto aid = 0;
    auto vid = 0;
    for (const auto& media : mediaList) {
        if (media[MediaAttributeKey::SOURCE] == source)
            break;
        if (media[MediaAttributeKey::MEDIA_TYPE] == MediaAttributeValue::AUDIO)
            aid++;
        if (media[MediaAttributeKey::MEDIA_TYPE] == MediaAttributeValue::VIDEO)
            vid++;
    }
    QString alabel = QString("audio_%1").arg(aid);
    QString vlabel = QString("video_%1").arg(vid);
    QString sep = libjami::Media::VideoProtocolPrefix::SEPARATOR;
    MapStringString audioMediaAttribute {};
    switch (type) {
    case MediaRequestType::FILESHARING: {
        // File sharing
        resource = !source.isEmpty() ? QString("%1%2%3")
                                           .arg(libjami::Media::VideoProtocolPrefix::FILE)
                                           .arg(sep)
                                           .arg(QUrl(source).toLocalFile())
                                     : libjami::Media::VideoProtocolPrefix::NONE;
        if (shareAudio)
            audioMediaAttribute = {{MediaAttributeKey::MEDIA_TYPE, MediaAttributeValue::AUDIO},
                                   {MediaAttributeKey::ENABLED, TRUE_STR},
                                   {MediaAttributeKey::MUTED, mute ? TRUE_STR : FALSE_STR},
                                   {MediaAttributeKey::SOURCE, resource},
                                   {MediaAttributeKey::LABEL, alabel}};
        break;
    }
    case MediaRequestType::SCREENSHARING: {
        // Screen/window sharing
        resource = source;
        break;
    }
    case MediaRequestType::CAMERA: {
        // Camera device
        resource = not source.isEmpty()
                       ? QString("%1%2%3").arg(libjami::Media::VideoProtocolPrefix::CAMERA).arg(sep).arg(source)
                       : libjami::Media::VideoProtocolPrefix::NONE;
        break;
    }
    default:
        return mediaList;
    }

    VectorMapStringString proposedList {};
    MapStringString videoMediaAttribute = {{MediaAttributeKey::MEDIA_TYPE, MediaAttributeValue::VIDEO},
                                           {MediaAttributeKey::ENABLED, TRUE_STR},
                                           {MediaAttributeKey::MUTED, mute ? TRUE_STR : FALSE_STR},
                                           {MediaAttributeKey::SOURCE, resource},
                                           {MediaAttributeKey::LABEL, vlabel}};
    // if we're in a 1:1, we only show one preview, so, limit to 1 video (the new one)
    auto participantsModel = pimpl_->participantsModel.find(callId);
    auto isConf = participantsModel != pimpl_->participantsModel.end()
                  && participantsModel->second->getParticipants().size() != 0;

    auto replaced = false;
    for (auto& media : mediaList) {
        auto replace = media[MediaAttributeKey::MEDIA_TYPE] == MediaAttributeValue::VIDEO;
        // In a 1:1 we replace the first video, in a conference we replace only if it's a muted
        // video or if a new sharing is requested
        if (isConf) {
            replace &= media[MediaAttributeKey::MUTED] == TRUE_STR;
            replace |= (media[MediaAttributeKey::SOURCE].startsWith(libjami::Media::VideoProtocolPrefix::FILE)
                        || media[MediaAttributeKey::SOURCE].startsWith(libjami::Media::VideoProtocolPrefix::DISPLAY))
                       && (type == MediaRequestType::FILESHARING || type == MediaRequestType::SCREENSHARING);
        }
        if (replace) {
            videoMediaAttribute[MediaAttributeKey::LABEL] = media[MediaAttributeKey::LABEL];
            media = videoMediaAttribute;
            replaced = true;
        }
        if (!(media[MediaAttributeKey::SOURCE].startsWith(libjami::Media::VideoProtocolPrefix::FILE)
              && type == MediaRequestType::CAMERA)) {
            proposedList.emplace_back(media);
        }
    }
    if (!replaced)
        proposedList.push_back(videoMediaAttribute);
    if (!audioMediaAttribute.isEmpty())
        proposedList.emplace_back(audioMediaAttribute);

    return proposedList;
}

void
CallModel::addMedia(const QString& callId, const QString& source, MediaRequestType type, bool mute, bool shareAudio)
{
    auto& callInfo = pimpl_->calls[callId];
    if (!callInfo || source.isEmpty())
        return;

    auto proposedList = getProposed(callInfo->mediaList, callId, source, type, mute, shareAudio);

    CallManager::instance().requestMediaChange(owner.id, callId, proposedList);
    callInfo->mediaList = proposedList;
    if (callInfo->status == call::Status::IN_PROGRESS)
        Q_EMIT callInfosChanged(owner.id, callId);
}

void
CallModel::removeMedia(
    const QString& callId, const QString& mediaType, const QString& type, bool muteCamera, bool removeAll)
{
    auto& callInfo = pimpl_->calls[callId];
    if (!callInfo)
        return;
    auto isVideo = mediaType == MediaAttributeValue::VIDEO;
    auto newIdx = 0;
    auto replaceIdx = false, hasVideo = false;
    VectorMapStringString proposedList;
    QString label;
    for (const auto& media : callInfo->mediaList) {
        if (media[MediaAttributeKey::MEDIA_TYPE] == mediaType && media[MediaAttributeKey::SOURCE].startsWith(type)) {
            replaceIdx = true;
            label = media[MediaAttributeKey::LABEL];
        } else {
            if (!removeAll || !media[MediaAttributeKey::SOURCE].startsWith(type)) {
                if (media[MediaAttributeKey::MEDIA_TYPE] == mediaType) {
                    auto newMedia = media;
                    if (replaceIdx) {
                        QString idxStr = QString::number(newIdx);
                        newMedia[MediaAttributeKey::LABEL] = isVideo ? "video_" + idxStr : "audio_" + idxStr;
                    }
                    proposedList.push_back(newMedia);
                    newIdx++;
                } else {
                    proposedList.push_back(media);
                }
            }
            hasVideo |= media[MediaAttributeKey::MEDIA_TYPE] == MediaAttributeValue::VIDEO;
        }
    }

    auto participantsModel = pimpl_->participantsModel.find(callId);
    auto isConf = participantsModel != pimpl_->participantsModel.end()
                  && participantsModel->second->getParticipants().size() != 0;
    if (!isConf) {
        // 1:1 call, in this case we only show one preview, and switch between sharing and camera
        // preview So, if no video, replace by camera
        if (!hasVideo) {
            proposedList = getProposed(proposedList,
                                       callInfo->id,
                                       pimpl_->lrc.getAVModel().getCurrentVideoCaptureDevice(),
                                       MediaRequestType::CAMERA,
                                       muteCamera);
        }
    } else if (!hasVideo) {
        // To receive the remote video, we need a muted camera
        proposedList.push_back(
            MapStringString {{MediaAttributeKey::MEDIA_TYPE, MediaAttributeValue::VIDEO},
                             {MediaAttributeKey::ENABLED, TRUE_STR},
                             {MediaAttributeKey::MUTED, TRUE_STR},
                             {MediaAttributeKey::SOURCE,
                              pimpl_->lrc.getAVModel()
                                  .getCurrentVideoCaptureDevice()}, // not needed to set the source. Daemon should be
                                                                    // able to check it
                             {MediaAttributeKey::LABEL, label.isEmpty() ? "video_0" : label}});
    }

    if (isVideo && !label.isEmpty())
        pimpl_->lrc.getAVModel().stopPreview(label);

    CallManager::instance().requestMediaChange(owner.id, callId, proposedList);
    callInfo->mediaList = proposedList;
    if (callInfo->status == call::Status::IN_PROGRESS)
        Q_EMIT callInfosChanged(owner.id, callId);
}

void
CallModel::accept(const QString& callId) const
{
    try {
        auto& callInfo = pimpl_->calls[callId];
        if (!callInfo)
            return;
        if (callInfo->mediaList.empty())
            CallManager::instance().accept(owner.id, callId);
        else
            CallManager::instance().acceptWithMedia(owner.id, callId, callInfo->mediaList);
    } catch (...) {
    }
}

void
CallModel::end(const QString& callId) const
{
    if (!hasCall(callId))
        return;
    auto& call = pimpl_->calls[callId];

    if (call->status == call::Status::INCOMING_RINGING) {
        CallManager::instance().decline(owner.id, callId);
        return;
    }

    switch (call->type) {
    case call::Type::DIALOG:
        CallManager::instance().end(owner.id, callId);
        break;
    case call::Type::CONFERENCE:
        CallManager::instance().endConference(owner.id, callId);
        break;
    case call::Type::INVALID:
    default:
        break;
    }
}

void
CallModel::decline(const QString& callId) const
{
    if (!hasCall(callId))
        return;
    CallManager::instance().decline(owner.id, callId);
}

void
CallModel::toggleAudioRecord(const QString& callId) const
{
    CallManager::instance().toggleRecording(owner.id, callId);
}

void
CallModel::playDTMF(const QString& callId, const QString& value) const
{
    if (!hasCall(callId))
        return;
    if (pimpl_->calls[callId]->status != call::Status::IN_PROGRESS)
        return;
    CallManager::instance().playDTMF(value);
}

void
CallModel::togglePause(const QString& callId) const
{
    // function should now only serves for SIP accounts
    if (!hasCall(callId))
        return;
    auto& call = pimpl_->calls[callId];

    if (call->status == call::Status::PAUSED) {
        if (call->type == call::Type::DIALOG) {
            CallManager::instance().resume(owner.id, callId);
        } else {
            CallManager::instance().resumeConference(owner.id, callId);
        }
    } else if (call->status == call::Status::IN_PROGRESS) {
        if (call->type == call::Type::DIALOG)
            CallManager::instance().hold(owner.id, callId);
        else {
            CallManager::instance().holdConference(owner.id, callId);
        }
    }
}

void
CallModel::setQuality(const QString& callId, const double quality) const
{
    Q_UNUSED(callId)
    Q_UNUSED(quality)
    qDebug() << "setQuality isn't implemented yet";
}

void
CallModel::transfer(const QString& callId, const QString& to) const
{
    CallManager::instance().transfer(owner.id, callId, to);
}

void
CallModel::transferToCall(const QString& callId, const QString& callIdDest) const
{
    CallManager::instance().attendedTransfer(owner.id, callId, callIdDest);
}

void
CallModel::joinCalls(const QString& callIdA, const QString& callIdB) const
{
    // Get call information
    call::Info call1, call2;
    QString accountIdCall1 = {}, accountIdCall2 = {};
    for (const auto& account_id : owner.accountModel->getAccountList()) {
        try {
            auto& accountInfo = owner.accountModel->getAccountInfo(account_id);
            if (accountInfo.callModel->hasCall(callIdA)) {
                call1 = accountInfo.callModel->getCall(callIdA);
                accountIdCall1 = account_id;
            }
            if (accountInfo.callModel->hasCall(callIdB)) {
                call2 = accountInfo.callModel->getCall(callIdB);
                accountIdCall2 = account_id;
            }
            if (!accountIdCall1.isEmpty() && !accountIdCall2.isEmpty())
                break;
        } catch (...) {
        }
    }
    if (accountIdCall1.isEmpty() || accountIdCall2.isEmpty()) {
        qWarning() << "Unable to join nonexistent calls.";
        return;
    }

    if (call1.type == call::Type::CONFERENCE && call2.type == call::Type::CONFERENCE) {
        bool joined = CallManager::instance().joinConference(accountIdCall1, callIdA, accountIdCall2, callIdB);

        if (!joined) {
            qWarning() << "Conference: " << callIdA << " is unable to join conference " << callIdB;
            return;
        }
        if (accountIdCall1 != owner.id) {
            // If the conference is added from another account
            try {
                auto& accountInfo = owner.accountModel->getAccountInfo(accountIdCall1);
                if (accountInfo.callModel->hasCall(callIdA)) {
                    Q_EMIT accountInfo.callModel->callAddedToConference(callIdA, "", callIdB);
                }
            } catch (...) {
            }
        } else {
            Q_EMIT callAddedToConference(callIdA, "", callIdB);
        }
    } else if (call1.type == call::Type::CONFERENCE || call2.type == call::Type::CONFERENCE) {
        auto call = call1.type == call::Type::CONFERENCE ? callIdB : callIdA;
        auto conf = call1.type == call::Type::CONFERENCE ? callIdA : callIdB;
        // Unpause conference if conference was not active
        CallManager::instance().resumeConference(owner.id, conf);
        auto accountCall = call1.type == call::Type::CONFERENCE ? accountIdCall2 : accountIdCall1;

        bool joined = CallManager::instance().addParticipant(accountCall, call, accountCall, conf);
        if (!joined) {
            qWarning() << "Call: " << call << " is unable to join conference " << conf;
            return;
        }
        if (accountCall != owner.id) {
            // If the call is added from another account
            try {
                auto& accountInfo = owner.accountModel->getAccountInfo(accountCall);
                if (accountInfo.callModel->hasCall(call)) {
                    accountInfo.callModel->pimpl_->slotConferenceCreated(owner.id, "", conf);
                }
            } catch (...) {
            }
        } else
            Q_EMIT callAddedToConference(call, "", conf);

        // Remove from pendingConferences_
        for (int i = 0; i < pimpl_->pendingConferencees_.size(); ++i) {
            if (pimpl_->pendingConferencees_.at(i).callId == call) {
                Q_EMIT beginRemovePendingConferenceesRows(i);
                pimpl_->pendingConferencees_.removeAt(i);
                Q_EMIT endRemovePendingConferenceesRows();
                break;
            }
        }
    } else {
        CallManager::instance().joinParticipant(accountIdCall1, callIdA, accountIdCall2, callIdB);
        // NOTE: This will trigger slotConferenceCreated.
    }
}

QString
CallModel::callAndAddParticipant(const QString uri, const QString& callId, bool audioOnly)
{
    // When adding a participant, only offer a single audio stream.
    // The existing call may have multiple audio streams, but new
    // participants should negotiate one audio stream with the conference
    // host, which handles mixing internally.
    VectorMapStringString mediaList;
    bool audioAdded = false;
    for (const auto& media : pimpl_->calls[callId]->mediaList) {
        if (media[libjami::Media::MediaAttributeKey::MEDIA_TYPE] == libjami::Media::MediaAttributeValue::AUDIO) {
            if (audioAdded)
                continue;
            audioAdded = true;
        }
        mediaList.push_back(media);
    }
    auto newCallId = createCall(uri, audioOnly, mediaList);
    Q_EMIT beginInsertPendingConferenceesRows(0);
    pimpl_->pendingConferencees_.prepend({uri, newCallId, callId});
    Q_EMIT endInsertPendingConferenceesRows();
    return newCallId;
}

void
CallModel::removeParticipant(const QString& callId, const QString& participant) const
{
    Q_UNUSED(callId)
    Q_UNUSED(participant)
    qDebug() << "removeParticipant() isn't implemented yet";
}

QString
CallModel::getFormattedCallDuration(const QString& callId) const
{
    if (!hasCall(callId))
        return "00:00";
    auto& startTime = pimpl_->calls[callId]->startTime;
    if (startTime.time_since_epoch().count() == 0)
        return "00:00";
    auto now = std::chrono::steady_clock::now();
    auto d = std::chrono::duration_cast<std::chrono::seconds>(now.time_since_epoch() - startTime.time_since_epoch())
                 .count();
    return interaction::getFormattedCallDuration(d);
}

bool
CallModel::isRecording(const QString& callId) const
{
    if (!hasCall(callId))
        return false;
    return CallManager::instance().getIsRecording(owner.id, callId);
}

QString
CallModel::getSIPCallStatusString(const short& statusCode)
{
    auto element = sip_call_status_code_map.find(statusCode);
    if (element != sip_call_status_code_map.end()) {
        return element->second;
    }
    return "";
}

const QList<call::PendingConferenceeInfo>&
CallModel::getPendingConferencees()
{
    return pimpl_->pendingConferencees_;
}

api::video::RenderedDevice
CallModel::getCurrentRenderedDevice(const QString& call_id) const
{
    video::RenderedDevice result;
    MapStringString callDetails;
    QStringList conferences = CallManager::instance().getConferenceList(owner.id);
    if (conferences.indexOf(call_id) != -1) {
        callDetails = CallManager::instance().getConferenceDetails(owner.id, call_id);
    } else {
        callDetails = CallManager::instance().getCallDetails(owner.id, call_id);
    }
    if (!callDetails.contains("VIDEO_SOURCE")) {
        return result;
    }
    auto source = callDetails["VIDEO_SOURCE"];
    auto sourceSize = source.size();
    if (source.startsWith("camera://")) {
        result.type = video::DeviceType::CAMERA;
        result.name = source.right(sourceSize - QString("camera://").size());
    } else if (source.startsWith("file://")) {
        result.type = video::DeviceType::FILE;
        result.name = source.right(sourceSize - QString("file://").size());
    } else if (source.startsWith("display://")) {
        result.type = video::DeviceType::DISPLAY;
        result.name = source.right(sourceSize - QString("display://").size());
    }
    return result;
}

std::optional<QString>
CallModel::computeFallbackConversation(bool currentIsCoreDialog,
                                       const QString& currentConversationUid,
                                       const QStringList& remoteParticipantUris,
                                       const QMap<QString, QString>& peerUriToConversationUid)
{
    if (!currentIsCoreDialog) {
        return std::nullopt;
    }

    if (remoteParticipantUris.isEmpty()) {
        return std::nullopt;
    }

    QString fallbackUid;

    for (const auto& uri : remoteParticipantUris) {
        auto it = peerUriToConversationUid.constFind(uri);
        if (it == peerUriToConversationUid.constEnd() || it.value().isEmpty()) {
            continue;
        }

        // If any remaining participant maps to the current conversation then no switch needed
        if (it.value() == currentConversationUid) {
            return std::nullopt;
        }

        // Otherwise remember the first available conversation candidate
        if (fallbackUid.isEmpty()) {
            fallbackUid = it.value();
        }
    }

    if (fallbackUid.isEmpty()) {
        return std::nullopt;
    }

    return fallbackUid;
}

QString
CallModel::getDisplay(int idx, int x, int y, int w, int h)
{
    QString sep = libjami::Media::VideoProtocolPrefix::SEPARATOR;
    return QString("%1%2:%3+%4,%5 %6x%7")
        .arg(libjami::Media::VideoProtocolPrefix::DISPLAY)
        .arg(sep)
        .arg(idx)
        .arg(x)
        .arg(y)
        .arg(w)
        .arg(h);
}

QString
CallModel::getDisplay(const QString& windowProcessId, const QString& windowId, const int fps)
{
#if defined(__APPLE__)
    Q_UNUSED(windowProcessId)
    Q_UNUSED(windowId)
    return {};
#else
    QString sep = libjami::Media::VideoProtocolPrefix::SEPARATOR;
    QString ret {};
#if defined(Q_OS_UNIX)
    Q_UNUSED(windowId);
    ret = QString("%1%2:+0,0 window-id:%3")
              .arg(libjami::Media::VideoProtocolPrefix::DISPLAY)
              .arg(sep)
              .arg(windowProcessId);
#elif WIN32
    ret = QString("%1%2:+0,0 window-id:hwnd=%3")
              .arg(libjami::Media::VideoProtocolPrefix::DISPLAY)
              .arg(sep)
              .arg(windowProcessId);
#endif
    // Usefull if the window is a preview to set a custom fps rate of 1
    if (fps > 0) {
        ret += QString(" fps:%1").arg(fps);
    }

    return ret;
#endif
}

CallModelPimpl::CallModelPimpl(const CallModel& linked,
                               Lrc& lrc,
                               const CallbacksHandler& callbacksHandler,
                               const BehaviorController& behaviorController)
    : linked(linked)
    , lrc(lrc)
    , callbacksHandler(callbacksHandler)
    , behaviorController(behaviorController)
{
    connect(&callbacksHandler, &CallbacksHandler::mediaChangeRequested, this, &CallModelPimpl::slotMediaChangeRequested);
    connect(&callbacksHandler, &CallbacksHandler::incomingCall, this, &CallModelPimpl::slotIncomingCall);
    connect(&callbacksHandler, &CallbacksHandler::callStateChanged, this, &CallModelPimpl::slotCallStateChanged);
    connect(&callbacksHandler,
            &CallbacksHandler::mediaNegotiationStatus,
            this,
            &CallModelPimpl::slotMediaNegotiationStatus);
    connect(&callbacksHandler, &CallbacksHandler::incomingVCardChunk, this, &CallModelPimpl::slotincomingVCardChunk);
    connect(&callbacksHandler, &CallbacksHandler::conferenceCreated, this, &CallModelPimpl::slotConferenceCreated);
    connect(&callbacksHandler, &CallbacksHandler::conferenceChanged, this, &CallModelPimpl::slotConferenceChanged);
    connect(&callbacksHandler, &CallbacksHandler::conferenceRemoved, this, &CallModelPimpl::slotConferenceRemoved);
    connect(&callbacksHandler, &CallbacksHandler::voiceMailNotify, this, &CallModelPimpl::slotVoiceMailNotify);
    connect(&CallManager::instance(),
            &CallManagerInterface::onConferenceInfosUpdated,
            this,
            &CallModelPimpl::slotOnConferenceInfosUpdated);
    connect(&callbacksHandler,
            &CallbacksHandler::remoteRecordingChanged,
            this,
            &CallModelPimpl::onRemoteRecordingChanged);
    connect(&callbacksHandler, &CallbacksHandler::recordingStateChanged, this, &CallModelPimpl::onRecordingStateChanged);
    connect(&callbacksHandler, &CallbacksHandler::recordingStateChanged, this, &CallModelPimpl::onRecordingStateChanged);

#ifndef ENABLE_LIBWRAP
    // Only necessary with D-Bus since the daemon runs separately
    initCallFromDaemon();
    initConferencesFromDaemon();
#endif
}

CallModelPimpl::~CallModelPimpl() {}

QVariantList
CallModelPimpl::callAdvancedInformation()
{
    QVariantList advancedInformationList;

    QStringList callList = CallManager::instance().getCallList(linked.owner.id);
    for (const auto& callId : callList) {
        MapStringString mapStringDetailsList = CallManager::instance().getCallDetails(linked.owner.id, callId);
        QVariantMap detailsList = mapStringStringToQVariantMap(mapStringDetailsList);

        detailsList.insert(CALL_ID, callId);
        detailsList.insert(HARDWARE_ACCELERATION, lrc.getAVModel().getHardwareAcceleration());
        advancedInformationList.append(detailsList);
    }

    return advancedInformationList;
}

MapStringString
CallModelPimpl::advancedInformationForCallId(QString callId)
{
    MapStringString infoMap = CallManager::instance().getCallDetails(linked.owner.id, callId);
    if (lrc.getAVModel().getHardwareAcceleration())
        infoMap[HARDWARE_ACCELERATION] = "True";
    else
        infoMap[HARDWARE_ACCELERATION] = "False";
    return infoMap;
}

OptRef<conversation::Info>
CallModelPimpl::getFallbackConversationForConference(const QString& confId)
{
    if (confId.isEmpty()) {
        qWarning() << "Empty conference ID provided.";
        return std::nullopt;
    }

    auto currentConversation = linked.owner.conversationModel->getConversationForCallId(confId);
    if (!currentConversation) {
        qWarning() << "No conversation found for conference ID:" << confId;
        return std::nullopt;
    }

    // Collect remote participants (excluding self, avoid duplicates)
    // participantsModel is populated from confInfos, which is entirely handled by the videomixer
    // and based on active RTP streams. It can be unreliable during transitions (e.g. camera activation)
    // where streams are temporarily missing.
    // Therefore a stable API from the daemon based on subcalls would be nice
    QStringList remoteParticipantsUris;
    auto participantIt = participantsModel.find(confId);
    if (participantIt != participantsModel.end() && participantIt->second) {
        for (const auto& participant : participantIt->second->getParticipants()) {
            auto uri = participant.uri;
            if (uri != linked.owner.profileInfo.uri && !remoteParticipantsUris.contains(uri)) {
                remoteParticipantsUris.append(uri);
            }
        }
    }

    QMap<QString, QString> peerUriToConversationUid;
    for (const auto& uri : remoteParticipantsUris) {
        const auto conv = linked.owner.conversationModel->getConversationForPeerUri(uri);
        if (conv) {
            peerUriToConversationUid.insert(uri, conv->get().uid);
        }
    }

    auto fallbackUid = CallModel::computeFallbackConversation(currentConversation->get().isCoreDialog(),
                                                              currentConversation->get().uid,
                                                              remoteParticipantsUris,
                                                              peerUriToConversationUid);
    if (!fallbackUid) {
        return std::nullopt;
    }

    auto fallbackConv = linked.owner.conversationModel->getConversationForUid(fallbackUid.value());
    if (!fallbackConv) {
        qWarning() << "Fallback conversation UID chosen but not found:" << fallbackUid.value();
        return std::nullopt;
    }

    qWarning() << "Fallback conversation found with UID:" << fallbackUid.value();
    return fallbackConv;
}

void
CallModelPimpl::applyFallbackConversationSwitch(const QString& confId,
                                                OptRef<conversation::Info> currentConversation,
                                                OptRef<conversation::Info> fallbackConversation,
                                                const QString& newCallId,
                                                const QString& newConfId)
{
    if (!fallbackConversation) {
        return;
    }

    if (currentConversation) {
        currentConversation->get().confId.clear();
    }

    fallbackConversation->get().confId = newConfId;
    fallbackConversation->get().callId = newCallId;

    qWarning() << "[conf:" << confId << "] Switching to conversation:" << fallbackConversation->get().uid;
    linked.owner.conversationModel->selectConversation(fallbackConversation->get().uid);
    Q_EMIT linked.fallbackConversationSelected(linked.owner.id, fallbackConversation->get().uid);
}

QStringList
CallModelPimpl::getCallIds()
{
    return CallManager::instance().getCallList(linked.owner.id);
}

void
CallModelPimpl::initCallFromDaemon()
{
    QStringList callList = CallManager::instance().getCallList(linked.owner.id);
    for (const auto& callId : callList) {
        MapStringString details = CallManager::instance().getCallDetails(linked.owner.id, callId);
        auto callInfo = std::make_shared<call::Info>();
        callInfo->id = callId;
        auto now = std::chrono::steady_clock::now();
        auto system_now = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now());
        auto diff = static_cast<int64_t>(system_now) - std::stol(details["TIMESTAMP_START"].toStdString());
        callInfo->startTime = now - std::chrono::seconds(diff);
        callInfo->status = call::to_status(details["CALL_STATE"]);
        auto endId = details["PEER_NUMBER"].indexOf("@");
        callInfo->peerUri = details["PEER_NUMBER"].left(endId);
        if (linked.owner.profileInfo.type == lrc::api::profile::Type::JAMI) {
            callInfo->peerUri = "ring:" + callInfo->peerUri;
        }
        callInfo->videoMuted = details["VIDEO_MUTED"] == TRUE_STR;
        callInfo->audioMuted = details["AUDIO_MUTED"] == TRUE_STR;
        callInfo->type = call::Type::DIALOG;
        VectorMapStringString infos = CallManager::instance().getConferenceInfos(linked.owner.id, callId);
        auto participantsPtr = std::make_shared<CallParticipants>(infos, callId, linked);
        callInfo->layout = participantsPtr->getLayout();
        participantsModel.emplace(callId, std::move(participantsPtr));
        calls.emplace(callId, std::move(callInfo));
        // NOTE/BUG: the videorenderer is unable to know that the client has restarted
        // So, for now, a user will have to manually restart the media until
        // this renderer is not redesigned.
    }
}

bool
CallModelPimpl::checkMediaDeviceMuted(const MapStringString& mediaAttributes)
{
    return mediaAttributes[MediaAttributeKey::SOURCE].startsWith("camera:")
           && (mediaAttributes[MediaAttributeKey::ENABLED] == FALSE_STR
               || mediaAttributes[MediaAttributeKey::MUTED] == TRUE_STR);
}

void
CallModelPimpl::initConferencesFromDaemon()
{
    QStringList callList = CallManager::instance().getConferenceList(linked.owner.id);
    for (const auto& callId : callList) {
        QMap<QString, QString> details = CallManager::instance().getConferenceDetails(linked.owner.id, callId);
        auto callInfo = std::make_shared<call::Info>();
        callInfo->id = callId;
        QStringList callList = CallManager::instance().getParticipantList(linked.owner.id, callId);
        Q_FOREACH (const auto& call, callList) {
            MapStringString callDetails = CallManager::instance().getCallDetails(linked.owner.id, call);
            auto now = std::chrono::steady_clock::now();
            auto system_now = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now());
            auto diff = static_cast<int64_t>(system_now) - std::stol(callDetails["TIMESTAMP_START"].toStdString());
            callInfo->status = details["CONF_STATE"] == "ACTIVE_ATTACHED" ? call::Status::IN_PROGRESS
                                                                          : call::Status::PAUSED;
            callInfo->startTime = now - std::chrono::seconds(diff);
            Q_EMIT linked.callAddedToConference(call, "", callId);
        }
        callInfo->type = call::Type::CONFERENCE;
        VectorMapStringString infos = CallManager::instance().getConferenceInfos(linked.owner.id, callId);
        auto participantsPtr = std::make_shared<CallParticipants>(infos, callId, linked);
        callInfo->layout = participantsPtr->getLayout();
        participantsModel.emplace(callId, std::move(participantsPtr));

        calls.emplace(callId, std::move(callInfo));
    }
}

void
CallModel::setCurrentCall(const QString& callId) const
{
    if (!pimpl_->manageCurrentCall_)
        return;
    auto it = std::find_if(pimpl_->pendingConferencees_.begin(),
                           pimpl_->pendingConferencees_.end(),
                           [callId](const lrc::api::call::PendingConferenceeInfo& info) -> bool {
                               return info.callId == callId;
                           });

    // Set current call only if not adding this call
    // to a current conference
    if (it != pimpl_->pendingConferencees_.end())
        return;
    if (!hasCall(callId))
        return;

    // The client should be able to set the current call multiple times
    if (pimpl_->currentCall_ == callId)
        return;
    pimpl_->currentCall_ = callId;

    // Resume call
    auto& call = pimpl_->calls[callId];
    if (call->status == call::Status::PAUSED) {
        auto& call = pimpl_->calls[callId];
        if (call->type == call::Type::DIALOG) {
            CallManager::instance().resume(owner.id, callId);
        } else {
            CallManager::instance().resumeConference(owner.id, callId);
        }
    }

    QStringList accountList = pimpl_->lrc.getAccountModel().getAccountList();
    // If we are setting a current call in the UI, we want to hold all other calls,
    // across accounts, to avoid sending our local media streams while another call
    // is in focus.
    for (const auto& acc : accountList) {
        VectorString filterCalls;
        // For each account, we should not hold calls linked to a conference
        QStringList conferences = CallManager::instance().getConferenceList(acc);
        for (const auto& confId : conferences) {
            QStringList callList = CallManager::instance().getParticipantList(acc, confId);
            Q_FOREACH (const auto& cid, callList) {
                filterCalls.push_back(cid);
            }
        }

        for (const auto& cid : Lrc::activeCalls(acc)) {
            auto filtered = std::find(filterCalls.begin(), filterCalls.end(), cid) != filterCalls.end();
            if (cid != callId && !filtered) {
                // Only hold calls for a non rendez-vous point
                CallManager::instance().hold(acc, cid);
            }
        }

        if (!lrc::api::Lrc::holdConferences) {
            continue;
        }
        // If the account is the host and it is attached to the conference,
        // then we should hold it.
        for (const auto& confId : conferences) {
            if (callId != confId) {
                MapStringString confDetails = CallManager::instance().getConferenceDetails(acc, confId);
                // Only hold conference if attached
                if (confDetails["CALL_STATE"] == "ACTIVE_DETACHED")
                    continue;
                QStringList callList = CallManager::instance().getParticipantList(acc, confId);
                if (callList.indexOf(callId) == -1)
                    CallManager::instance().holdConference(acc, confId);
            }
        }
    }

    Q_EMIT currentCallChanged(callId);
}

void
CallModel::setConferenceLayout(const QString& confId, const call::Layout& layout)
{
    auto call = pimpl_->calls.find(confId);
    if (call != pimpl_->calls.end()) {
        switch (layout) {
        case call::Layout::GRID:
            CallManager::instance().setConferenceLayout(owner.id, confId, 0);
            break;
        case call::Layout::ONE_WITH_SMALL:
            CallManager::instance().setConferenceLayout(owner.id, confId, 1);
            break;
        case call::Layout::ONE:
            CallManager::instance().setConferenceLayout(owner.id, confId, 2);
            break;
        }
        call->second->layout = layout;
    }
}

void
CallModel::setActiveStream(
    const QString& confId, const QString& accountUri, const QString& deviceId, const QString& streamId, bool state)
{
    CallManager::instance().setActiveStream(owner.id, confId, accountUri, deviceId, streamId, state);
}

bool
CallModel::isModerator(const QString& confId, const QString& uri)
{
    auto call = pimpl_->calls.find(confId);
    if (call == pimpl_->calls.end() or not call->second)
        return false;
    auto participantsModel = pimpl_->participantsModel.find(confId);
    if (participantsModel == pimpl_->participantsModel.end() or participantsModel->second->getParticipants().size() == 0)
        return true;
    auto ownerUri = owner.profileInfo.uri;
    auto uriToCheck = uri;
    if (uriToCheck.isEmpty()) {
        uriToCheck = ownerUri;
    }
    auto isModerator = uriToCheck == ownerUri ? call->second->type == lrc::api::call::Type::CONFERENCE : false;
    if (!isModerator && participantsModel->second->getParticipants().size() != 0) {
        if (!uri.isEmpty())
            isModerator = participantsModel->second->checkModerator(uri);
        else
            isModerator = participantsModel->second->checkModerator(owner.profileInfo.uri);
    }
    return isModerator;
}

void
CallModel::setModerator(const QString& confId, const QString& peerId, const bool& state)
{
    CallManager::instance().setModerator(owner.id, confId, peerId, state);
}

bool
CallModel::isHandRaised(const QString& confId, const QString& uri) noexcept
{
    auto call = pimpl_->calls.find(confId);
    if (call == pimpl_->calls.end() or not call->second)
        return false;

    auto participantsModel = pimpl_->participantsModel.find(confId);
    if (participantsModel == pimpl_->participantsModel.end())
        return false;

    auto ownerUri = owner.profileInfo.uri;
    auto uriToCheck = uri;
    if (uriToCheck.isEmpty()) {
        uriToCheck = ownerUri;
    }
    auto handRaised = false;
    for (const auto& participant : participantsModel->second->getParticipants()) {
        if (participant.uri == uriToCheck) {
            handRaised = participant.handRaised;
            break;
        }
    }
    return handRaised;
}

void
CallModel::raiseHand(const QString& confId, const QString& accountUri, const QString& deviceId, bool state)
{
    CallManager::instance().raiseHand(owner.id, confId, accountUri, deviceId, state);
}

void
CallModel::muteStream(const QString& confId,
                      const QString& accountUri,
                      const QString& deviceId,
                      const QString& streamId,
                      const bool& state)
{
    CallManager::instance().muteStream(owner.id, confId, accountUri, deviceId, streamId, state);
}

void
CallModel::disconnectParticipant(const QString& confId, const QString& accountUri, const QString& deviceId)
{
    CallManager::instance().disconnectParticipant(owner.id, confId, accountUri, deviceId);
}

void
CallModel::sendSipMessage(const QString& callId, const QString& body) const
{
    MapStringString payloads;
    payloads[TEXT_PLAIN] = body;

    CallManager::instance().sendTextMessage(owner.id, callId, payloads, true /* not used */);
}

bool
CallModel::isConferenceHost(const QString& callId)
{
    auto call = pimpl_->calls.find(callId);
    if (call == pimpl_->calls.end() or not call->second)
        return false;
    else
        return call->second->type == lrc::api::call::Type::CONFERENCE;
}

void
CallModelPimpl::slotMediaChangeRequested(const QString& accountId,
                                         const QString& callId,
                                         const VectorMapStringString& mediaList)
{
    if (linked.owner.id != accountId) {
        return;
    }

    if (mediaList.empty())
        return;

    auto& callInfo = calls[callId];
    if (!callInfo)
        return;

    QList<QString> currentMediaLabels {};
    for (auto& currentItem : callInfo->mediaList)
        currentMediaLabels.append(currentItem[MediaAttributeKey::LABEL]);

    auto answerMedia = QList<MapStringString>::fromVector(mediaList);

    for (auto& item : answerMedia) {
        int index = currentMediaLabels.indexOf(item[MediaAttributeKey::LABEL]);
        if (index >= 0) {
            item[MediaAttributeKey::MUTED] = callInfo->mediaList[index][MediaAttributeKey::MUTED];
            item[MediaAttributeKey::ENABLED] = callInfo->mediaList[index][MediaAttributeKey::ENABLED];
        } else {
            item[MediaAttributeKey::MUTED] = TRUE_STR;
            item[MediaAttributeKey::ENABLED] = TRUE_STR;
        }
    }
    CallManager::instance().answerMediaChangeRequest(linked.owner.id,
                                                     callId,
                                                     QVector<MapStringString>::fromList(answerMedia));
}

void
CallModelPimpl::slotIncomingCall(const QString& accountId,
                                 const QString& callId,
                                 const QString& from,
                                 const VectorMapStringString& mediaList)
{
    if (accountId != linked.owner.id)
        return;

    if (linked.hasCall(callId)) {
        qWarning() << "Did not initialize incoming call" << callId << "because it already exists";
        return;
    }

    auto callInfo = std::make_shared<call::Info>();
    callInfo->id = callId;

    MapStringString details = CallManager::instance().getCallDetails(linked.owner.id, callId);

    if (details.isEmpty()) {
        callInfo->peerUri = from;
        callInfo->isOutgoing = false;
        callInfo->status = call::Status::INCOMING_RINGING;
        callInfo->type = call::Type::DIALOG;
        bool hasVideo = false;
        for (const auto& media : mediaList) {
            if (media.value(MediaAttributeKey::MEDIA_TYPE) == MediaAttributeValue::VIDEO) {
                hasVideo = true;
                break;
            }
        }
        callInfo->isAudioOnly = !hasVideo;
        callInfo->videoMuted = !hasVideo;
        initializeMediaList(callInfo->mediaList, callInfo->isAudioOnly, callInfo->videoMuted);
    } else {
        auto endId = details["PEER_NUMBER"].indexOf("@");
        callInfo->peerUri = details["PEER_NUMBER"].left(endId);
        callInfo->isOutgoing = details["CALL_TYPE"] == "1";
        callInfo->status = call::Status::INCOMING_RINGING;
        callInfo->type = call::Type::DIALOG;
        callInfo->isAudioOnly = details["AUDIO_ONLY"] == TRUE_STR;
        callInfo->videoMuted = details["VIDEO_MUTED"] == TRUE_STR;
        initializeMediaList(callInfo->mediaList, callInfo->isAudioOnly, callInfo->videoMuted);
    }

    calls.emplace(callId, std::move(callInfo));

    if (!(details["CALL_TYPE"] == "1") && !linked.owner.confProperties.allowIncoming
        && linked.owner.profileInfo.type == profile::Type::JAMI) {
        linked.decline(callId);
        return;
    }

    QString displayname;
    QString peerId;
    QString peerUri = details.isEmpty() ? from : details["PEER_NUMBER"];
    QString toUsername = details.isEmpty() ? "" : details["TO_USERNAME"];

    if (peerUri.contains("ring.dht")) {
        peerId = peerUri.right(50);
        peerId = peerId.left(40);
        if (!details.isEmpty())
            displayname = details["REGISTERED_NAME"];
    } else {
        auto left = std::max(peerUri.indexOf("<"), peerUri.indexOf(":")) + 1;
        auto right = peerUri.indexOf("@");
        right = std::max(right, peerUri.indexOf(">"));
        peerId = peerUri.mid(left, right - left);
        if (!details.isEmpty())
            displayname = details["DISPLAY_NAME"];
        if (displayname.isEmpty())
            displayname = peerId;
    }

    qDebug() << "Initialized incoming call" << callId << "from peer" << peerId;

    Q_EMIT linked.newCall(peerId, callId, displayname, false, toUsername);

    Q_EMIT linked.callStatusChanged(accountId, callId, 0);
    Q_EMIT behaviorController.callStatusChanged(linked.owner.id, callId);
}

void
CallModelPimpl::slotCallStateChanged(const QString& accountId, const QString& callId, const QString& state, int code)
{
    if (accountId != linked.owner.id)
        return;

    if (!linked.hasCall(callId)) {
        qDebug() << "slotCallStateChanged: unknown callId " << callId;
        return;
    }

    auto status = call::to_status(state);
    auto& call = calls[callId];
    if (!call)
        return;

    if (status == call::Status::ENDED && !call::isTerminating(call->status)) {
        call->status = call::Status::TERMINATING;
        Q_EMIT linked.callStatusChanged(accountId, callId, code);
        Q_EMIT behaviorController.callStatusChanged(linked.owner.id, callId);
    }

    // proper state transition
    auto previousStatus = call->status;
    call->status = status;

    if (previousStatus == call->status) {
        // call state didn't change, simply ignore signal
        return;
    }

    qDebug() << QString("slotCallStateChanged (call: %1), from %2 to %3")
                    .arg(callId)
                    .arg(call::to_string(previousStatus))
                    .arg(call::to_string(status));

    // NOTE: signal emission order matters, always emit CallStatusChanged before CallEnded
    Q_EMIT linked.callStatusChanged(accountId, callId, code);
    Q_EMIT behaviorController.callStatusChanged(linked.owner.id, callId);

    if (call->status == call::Status::ENDED) {
        Q_EMIT linked.callEnded(callId, code);

        // Remove from pendingConferences_
        for (int i = 0; i < pendingConferencees_.size(); ++i) {
            if (pendingConferencees_.at(i).callId == callId) {
                Q_EMIT linked.beginRemovePendingConferenceesRows(i);
                pendingConferencees_.removeAt(i);
                Q_EMIT linked.endRemovePendingConferenceesRows();
                break;
            }
        }
    } else if (call->status == call::Status::IN_PROGRESS) {
        if (previousStatus == call::Status::INCOMING_RINGING || previousStatus == call::Status::OUTGOING_RINGING) {
            call->startTime = std::chrono::steady_clock::now();
            Q_EMIT linked.callStarted(callId);
        }
        // Add to calls if in pendingConferences_
        for (int i = 0; i < pendingConferencees_.size(); ++i) {
            if (pendingConferencees_.at(i).callId == callId) {
                linked.joinCalls(pendingConferencees_.at(i).callIdToJoin, pendingConferencees_.at(i).callId);
                break;
            }
        }
    } else if (call->status == call::Status::PAUSED) {
        currentCall_ = "";
    }
}

void
CallModelPimpl::slotMediaNegotiationStatus(const QString& callId, const QString&, const VectorMapStringString& mediaList)
{
    if (!linked.hasCall(callId)) {
        return;
    }

    auto& callInfo = calls[callId];
    if (!callInfo) {
        return;
    }

    callInfo->isAudioOnly = true;
    callInfo->videoMuted = true;
    for (const auto& item : mediaList) {
        if (item[MediaAttributeKey::MEDIA_TYPE] == MediaAttributeValue::VIDEO) {
            if (item[MediaAttributeKey::ENABLED] == TRUE_STR) {
                callInfo->isAudioOnly = false;
            }
            callInfo->videoMuted = checkMediaDeviceMuted(item);
        }
        if (item[MediaAttributeKey::MEDIA_TYPE] == MediaAttributeValue::AUDIO) {
            callInfo->audioMuted = checkMediaDeviceMuted(item);
        }
    }
    callInfo->mediaList = mediaList;
    if (callInfo->status == call::Status::IN_PROGRESS)
        Q_EMIT linked.callInfosChanged(linked.owner.id, callId);
}

void
CallModelPimpl::slotincomingVCardChunk(const QString& accountId,
                                       const QString& callId,
                                       const QString& from,
                                       int part,
                                       int numberOfParts,
                                       const QString& payload)
{
    if (accountId != linked.owner.id || !linked.hasCall(callId))
        return;

    auto it = vcardsChunks.find(from);
    if (it != vcardsChunks.end()) {
        vcardsChunks[from][part - 1] = payload;

        if (not std::any_of(vcardsChunks[from].begin(), vcardsChunks[from].end(), [](const auto& s) {
                return s.isEmpty();
            })) {
            profile::Info profileInfo;
            profileInfo.uri = from;
            profileInfo.type = profile::Type::JAMI;

            QString vcardPhoto;

            for (auto& chunk : vcardsChunks[from])
                vcardPhoto += chunk;

            for (auto& e : QString(vcardPhoto).split("\n"))
                if (e.contains("PHOTO"))
                    profileInfo.avatar = e.split(":")[1];
                else if (e.contains("FN"))
                    profileInfo.alias = e.split(":")[1];

            contact::Info contactInfo;
            contactInfo.profileInfo = profileInfo;

            linked.owner.contactModel->addContact(contactInfo);
            contactInfo.profileInfo.avatar.clear(); // Do not want avatar in memory here
            vcardsChunks.erase(from);               // Transfer is finish, we don't want to reuse this entry.
        }
    } else {
        vcardsChunks[from] = VectorString(numberOfParts);
        vcardsChunks[from][part - 1] = payload;
    }
}

void
CallModelPimpl::slotVoiceMailNotify(const QString& accountId, int newCount, int oldCount, int urgentCount)
{
    Q_EMIT linked.voiceMailNotify(accountId, newCount, oldCount, urgentCount);
}

void
CallModelPimpl::slotOnConferenceInfosUpdated(const QString& confId, const VectorMapStringString& infos)
{
    auto it = calls.find(confId);
    if (it == calls.end() or not it->second)
        return;

    // TODO: remove when the rendez-vous UI will be done
    // For now, the rendez-vous account can see ongoing calls
    // And must be notified when a new
    QStringList callList = CallManager::instance().getParticipantList(linked.owner.id, confId);
    const auto callListSize = static_cast<size_t>(callList.size());
    const auto previousCallListSizeIt = previousCallListSizes_.find(confId);
    const auto previousCallListSize = previousCallListSizeIt != previousCallListSizes_.end()
                                          ? previousCallListSizeIt->second
                                          : 0;
    previousCallListSizes_[confId] = callListSize;

    qDebug() << "[conf:" << confId << "] Conference infos updated. Calls remaining:" << callListSize;
    Q_FOREACH (const auto& call, callList) {
        Q_EMIT linked.callAddedToConference(call, "", confId);
        if (calls.find(call) == calls.end()) {
            qWarning() << "Call not found";
        } else {
            calls[call]->videoMuted = it->second->videoMuted;
            calls[call]->audioMuted = it->second->audioMuted;
            Q_EMIT linked.callInfosChanged(linked.owner.id, call);
        }
    }

    auto participantIt = participantsModel.find(confId);
    if (participantIt == participantsModel.end())
        participantIt = participantsModel.emplace(confId, std::make_shared<CallParticipants>(infos, confId, linked))
                            .first;
    else
        participantIt->second->update(infos);
    it->second->layout = participantIt->second->getLayout();

    // if Jami, remove @ring.dht
    for (auto& participant : participantIt->second->getParticipants()) {
        participant.uri.replace("@ring.dht", "");
        if (participant.uri.isEmpty()) {
            if (it->second->type == call::Type::CONFERENCE) {
                participant.uri = linked.owner.profileInfo.uri;
            } else {
                participant.uri = it->second->peerUri.replace("ring:", "");
            }
        }
    }

    // If the conference only has 1 subcall left and getting destroyed, it will be handled by slotConferenceRemoved
    // Otherwise, if the callist size decreased, we need to check if we need to switch conversation
    bool participantHasLeft = previousCallListSize > callListSize;
    bool conferenceIsOngoing = callListSize > 1;
    if (participantHasLeft && conferenceIsOngoing) {
        if (auto fallbackConversation = getFallbackConversationForConference(confId)) {
            auto currentConversation = linked.owner.conversationModel->getConversationForCallId(confId);
            applyFallbackConversationSwitch(confId, currentConversation, fallbackConversation, confId, confId);
        }
    }

    for (auto& participantInfo : infos) {
        if (participantInfo["uri"].isEmpty()) {
            it->second->videoMuted = participantInfo["videoMuted"] == TRUE_STR;
            it->second->audioMuted = participantInfo["audioLocalMuted"] == TRUE_STR;
        }
    }

    Q_EMIT linked.callInfosChanged(linked.owner.id, confId);
    Q_EMIT linked.participantsChanged(confId);
}

bool
CallModel::hasCall(const QString& callId) const
{
    return pimpl_->calls.find(callId) != pimpl_->calls.end();
}

void
CallModelPimpl::slotConferenceCreated(const QString& accountId, const QString& conversationId, const QString& confId)
{
    if (accountId != linked.owner.id)
        return;

    auto callInfo = std::make_shared<call::Info>();
    callInfo->id = confId;
    callInfo->status = call::Status::IN_PROGRESS;
    callInfo->type = call::Type::CONFERENCE;
    callInfo->startTime = std::chrono::steady_clock::now();

    VectorMapStringString infos = CallManager::instance().getConferenceInfos(linked.owner.id, confId);
    auto participantsPtr = std::make_shared<CallParticipants>(infos, confId, linked);
    callInfo->layout = participantsPtr->getLayout();
    VectorMapStringString mediaList = CallManager::instance().currentMediaList(linked.owner.id, confId);
    callInfo->mediaList = mediaList;
    participantsModel[confId] = participantsPtr;

    calls[confId] = callInfo;

    QString currentCallId = currentCall_;
    if (!conversationId.isEmpty()) {
        Q_EMIT linked.callAddedToConference("", conversationId, confId);
        if (currentCall_ != confId && waitForConference_.contains(conversationId)) {
            currentCall_ = confId;
            Q_EMIT linked.currentCallChanged(confId);
        }
    } else {
        QStringList callList = CallManager::instance().getParticipantList(linked.owner.id, confId);
        Q_FOREACH (const auto& call, callList) {
            Q_EMIT linked.callAddedToConference(call, "", confId);
            // Remove call from pendingConferences_
            for (int i = 0; i < pendingConferencees_.size(); ++i) {
                if (pendingConferencees_.at(i).callId == call) {
                    Q_EMIT linked.beginRemovePendingConferenceesRows(i);
                    pendingConferencees_.removeAt(i);
                    Q_EMIT linked.endRemovePendingConferenceesRows();
                    break;
                }
            }
            if (call == currentCall_)
                currentCall_ = confId;
        }
        if (currentCallId != currentCall_)
            Q_EMIT linked.currentCallChanged(confId);
    }
}

void
CallModelPimpl::slotConferenceChanged(const QString& accountId, const QString& confId, const QString&)
{
    if (accountId != linked.owner.id)
        return;
    // Detect if conference is created for this account
    QStringList callList = CallManager::instance().getParticipantList(linked.owner.id, confId);
    QString currentCallId = currentCall_;
    Q_FOREACH (const auto& call, callList) {
        Q_EMIT linked.callAddedToConference(call, "", confId);
        if (call == currentCall_)
            currentCall_ = confId;
    }
    Q_EMIT linked.currentCallChanged(currentCall_);
}

void
CallModelPimpl::slotConferenceRemoved(const QString& accountId, const QString& confId)
{
    if (accountId != linked.owner.id || !linked.owner.conversationModel) {
        return;
    }

    auto confIt = calls.find(confId);
    if (confIt == calls.end() || !confIt->second) {
        return;
    }

    qWarning() << "[conf:" << confId
               << "] Destroying conference, determining whether a conversation switch is necessary.";

    QString remainingCallId;

    // Find the remaining call's id
    auto participantIt = participantsModel.find(confId);
    if (participantIt != participantsModel.end() && participantIt->second) {
        for (const auto& participant : participantIt->second->getParticipants()) {
            if (participant.uri != linked.owner.profileInfo.uri) {
                try {
                    remainingCallId = linked.getCallFromURI(participant.uri, true).id;
                    break;
                } catch (const std::out_of_range&) {
                    qWarning() << "No call found for participant" << participant.uri;
                }
            }
        }
    } else if (participantIt == participantsModel.end()) {
        qWarning() << "[conf:" << confId << "] No participants model found for conference.";
    } else {
        qWarning() << "[conf:" << confId << "] Participants model is null.";
    }

    auto currentConversation = linked.owner.conversationModel->getConversationForCallId(confId);
    auto fallbackConversation = getFallbackConversationForConference(confId);

    // If a fallback conversation was returned, we should switch to it
    if (fallbackConversation) {
        applyFallbackConversationSwitch(confId, currentConversation, fallbackConversation, remainingCallId, QString {});
    } else if (currentConversation) {
        currentConversation->get().confId.clear();
        currentConversation->get().callId = remainingCallId;
        qWarning() << "[conf:" << confId << "] Staying in current conversation:" << currentConversation->get().uid;
    } else {
        qWarning() << "[conf:" << confId << "] No conversation to switch to";
    }

    // Now remove the conference from our models and list of calls then update currentCall_
    participantsModel.erase(confId);
    calls.erase(confIt);
    previousCallListSizes_.erase(confId);

    currentCall_ = remainingCallId;
    Q_EMIT linked.currentCallChanged(currentCall_);

    qDebug() << "[conf:" << confId << "] Conference removed, now staying on call" << currentCall_;
}

void
CallModelPimpl::onRemoteRecordingChanged(const QString& callId, const QString& peerUri, bool state)
{
    auto it = calls.find(callId);
    if (it == calls.end() or !it->second) {
        return;
    }

    auto uri = peerUri;

    if (uri.contains("ring:"))
        uri.remove("ring:");
    if (uri.contains("jami:"))
        uri.remove("jami:");
    if (uri.contains("@ring.dht"))
        uri.remove("@ring.dht");

    // Add/remove peer to recordingPeers, preventing duplicates.
    if (state && !it->second->recordingPeers.contains(uri))
        it->second->recordingPeers.append(uri);
    else if (!state && it->second->recordingPeers.contains(uri))
        it->second->recordingPeers.removeAll(uri);

    Q_EMIT linked.remoteRecordersChanged(callId, it->second->recordingPeers);
}

void
CallModelPimpl::onRecordingStateChanged(const QString& callId, bool state)
{
    Q_EMIT linked.recordingStateChanged(callId, state);
}

} // namespace lrc

#include "api/moc_callmodel.cpp"
#include "callmodel.moc"
