/***************************************************************************
 *   SPDX-FileCopyrightText: 2006 Tobias Koenig <tokoe@kde.org>            *
 *   SPDX-FileCopyrightText: 2007 Volker Krause <vkrause@kde.org>          *
 *                                                                         *
 *   SPDX-License-Identifier: LGPL-2.0-or-later                            *
 ***************************************************************************/

#include "agentmanager.h"

#include "agentbrokeninstance.h"
#include "agentmanageradaptor.h"
#include "agentmanagerinternaladaptor.h"
#include "agentprocessinstance.h"
#include "agentserverinterface.h"
#include "agentthreadinstance.h"
#include "akonadicontrol_debug.h"
#include "preprocessor_manager.h"
#include "processcontrol.h"
#include "resource_manager.h"
#include "serverinterface.h"

#include "private/dbus_p.h"
#include "private/instance_p.h"
#include "private/protocol_p.h"
#include "private/standarddirs_p.h"

#include "shared/akapplication.h"

#include <QCoreApplication>
#include <QDBusConnection>
#include <QDir>
#include <QScopedPointer>
#include <QSettings>

using Akonadi::ProcessControl;
using namespace std::chrono_literals;

static const bool enableAgentServerDefault = false;

// Here lie the once mighty resources, now deprecated and voided. They battled
// valiantly against crashes and segfaults, and now, they can finally enjoy
// their eternal null pointers. We'll miss you (not), but let's face it,
// your stacks have overflown a long time ago. Farewell, old code comrades and
// may your functions forever return void!
//
// Those are agent types that akonadicontrol can safely remove if it finds any leftover
// instances of them configured in the agentsrc file or on Akonadi Server.
static const QStringView retiredAgentTypes[] = {
    u"akonadi_kolab_resource",
    u"akonadi_akonotes_resource",
    u"akonadi_notes_resource",
    u"akonadi_notes_agent",
};

class StorageProcessControl : public Akonadi::ProcessControl
{
    Q_OBJECT
public:
    explicit StorageProcessControl(const QStringList &args)
    {
        setShutdownTimeout(15s);
        connect(this, &Akonadi::ProcessControl::unableToStart, this, []() {
            QCoreApplication::instance()->exit(255);
        });
        start(QStringLiteral("akonadiserver"), args, RestartOnCrash);
    }

    ~StorageProcessControl() override
    {
        setCrashPolicy(ProcessControl::StopOnCrash);
        org::freedesktop::Akonadi::Server serverIface(Akonadi::DBus::serviceName(Akonadi::DBus::Server),
                                                      QStringLiteral("/Server"),
                                                      QDBusConnection::sessionBus());
        serverIface.quit();
    }
};

class AgentServerProcessControl : public Akonadi::ProcessControl
{
    Q_OBJECT
public:
    explicit AgentServerProcessControl(const QStringList &args)
    {
        connect(this, &Akonadi::ProcessControl::unableToStart, this, []() {
            qCCritical(AKONADICONTROL_LOG) << "Failed to start AgentServer!";
        });
        start(QStringLiteral("akonadi_agent_server"), args, RestartOnCrash);
    }

    ~AgentServerProcessControl() override
    {
        setCrashPolicy(ProcessControl::StopOnCrash);
        org::freedesktop::Akonadi::AgentServer agentServerIface(Akonadi::DBus::serviceName(Akonadi::DBus::AgentServer),
                                                                QStringLiteral("/AgentServer"),
                                                                QDBusConnection::sessionBus(),
                                                                this);
        agentServerIface.quit();
    }
};

AgentManager::AgentManager(bool verbose, QObject *parent)
    : QObject(parent)
    , mAgentServer(nullptr)
    , mVerbose(verbose)
{
    new AgentManagerAdaptor(this);
    new AgentManagerInternalAdaptor(this);
    QDBusConnection::sessionBus().registerObject(QStringLiteral("/AgentManager"), this);

    connect(QDBusConnection::sessionBus().interface(), &QDBusConnectionInterface::serviceOwnerChanged, this, &AgentManager::serviceOwnerChanged);

    if (QDBusConnection::sessionBus().interface()->isServiceRegistered(Akonadi::DBus::serviceName(Akonadi::DBus::Server))) {
        qFatal("akonadiserver already running!");
    }

    const QSettings settings(Akonadi::StandardDirs::agentsConfigFile(Akonadi::StandardDirs::ReadOnly), QSettings::IniFormat);
    mAgentServerEnabled = settings.value(QStringLiteral("AgentServer/Enabled"), enableAgentServerDefault).toBool();

    QStringList serviceArgs;
    if (Akonadi::Instance::hasIdentifier()) {
        serviceArgs << QStringLiteral("--instance") << Akonadi::Instance::identifier();
    }
    if (verbose) {
        serviceArgs << QStringLiteral("--verbose");
    }

    mStorageController = std::unique_ptr<Akonadi::ProcessControl>(new StorageProcessControl(serviceArgs));

    if (mAgentServerEnabled) {
        mAgentServer = std::unique_ptr<Akonadi::ProcessControl>(new AgentServerProcessControl(serviceArgs));
    }
}

void AgentManager::continueStartup()
{
    // prevent multiple calls in case the server has to be restarted
    static bool first = true;
    if (!first) {
        return;
    }

    first = false;

    readPluginInfos();
    for (const AgentType &info : std::as_const(mAgents)) {
        Q_EMIT agentTypeAdded(info.identifier);
    }

    load();
    for (const AgentType &info : std::as_const(mAgents)) {
        ensureAutoStart(info);
    }
    mAllAgentsStarted = true;

    // we might already have autostarted zero out of zero agents, in which case we'd possibly get no calls to this, so call it now
    registerServiceIfDoneWaitingForAgents();
}

AgentManager::~AgentManager()
{
    cleanup();
}

void AgentManager::cleanup()
{
    for (const AgentInstance::Ptr &instance : std::as_const(mAgentInstances)) {
        instance->quit();
    }
    mAgentInstances.clear();

    mStorageController.reset();
    mStorageController.reset();
}

QStringList AgentManager::agentTypes() const
{
    return mAgents.keys();
}

QString AgentManager::agentName(const QString &identifier) const
{
    if (!checkAgentExists(identifier)) {
        return QString();
    }

    return mAgents.value(identifier).name;
}

QString AgentManager::agentComment(const QString &identifier) const
{
    if (!checkAgentExists(identifier)) {
        return QString();
    }

    return mAgents.value(identifier).comment;
}

QString AgentManager::agentIcon(const QString &identifier) const
{
    if (!checkAgentExists(identifier)) {
        return QString();
    }

    const AgentType info = mAgents.value(identifier);
    if (!info.icon.isEmpty()) {
        return info.icon;
    }

    return QStringLiteral("application-x-executable");
}

QStringList AgentManager::agentMimeTypes(const QString &identifier) const
{
    if (!checkAgentExists(identifier)) {
        return QStringList();
    }

    return mAgents.value(identifier).mimeTypes;
}

QStringList AgentManager::agentCapabilities(const QString &identifier) const
{
    if (!checkAgentExists(identifier)) {
        return QStringList();
    }
    return mAgents.value(identifier).capabilities;
}

QVariantMap AgentManager::agentCustomProperties(const QString &identifier) const
{
    if (!checkAgentExists(identifier)) {
        return QVariantMap();
    }

    return mAgents.value(identifier).custom;
}

AgentInstance::Ptr AgentManager::createAgentInstance(const AgentType &info)
{
    switch (info.launchMethod) {
    case AgentType::Server:
        return QSharedPointer<Akonadi::AgentThreadInstance>::create(*this);
    case AgentType::Launcher: // Fall through
    case AgentType::Process:
        return QSharedPointer<Akonadi::AgentProcessInstance>::create(*this);
    default:
        Q_ASSERT_X(false, "AgentManger::createAgentInstance", "Unhandled AgentType::LaunchMethod case");
    }

    return AgentInstance::Ptr();
}

QString AgentManager::createAgentInstance(const QString &identifier)
{
    if (!checkAgentExists(identifier)) {
        return QString();
    }

    const AgentType agentInfo = mAgents[identifier];
    mAgents[identifier].instanceCounter++;

    const auto instance = createAgentInstance(agentInfo);
    if (agentInfo.capabilities.contains(AgentType::CapabilityUnique)) {
        instance->setIdentifier(identifier);
    } else {
        instance->setIdentifier(QStringLiteral("%1_%2").arg(identifier, QString::number(agentInfo.instanceCounter)));
    }

    const QString instanceIdentifier = instance->identifier();
    if (mAgentInstances.contains(instanceIdentifier)) {
        qCWarning(AKONADICONTROL_LOG) << "Cannot create another instance of agent" << identifier;
        return QString();
    }

    // Return from this dbus call before we do the next. Otherwise dbus brakes for
    // this process.
    if (calledFromDBus()) {
        connection().send(message().createReply(instanceIdentifier));
    }

    if (!instance->start(agentInfo)) {
        return QString();
    }

    mAgentInstances.insert(instanceIdentifier, instance);
    registerAgentAtServer(instanceIdentifier, agentInfo);
    save();

    return instanceIdentifier;
}

void AgentManager::removeAgentInstance(const QString &identifier)
{
    const auto instance = mAgentInstances.value(identifier);
    if (!instance) {
        qCWarning(AKONADICONTROL_LOG) << Q_FUNC_INFO << "Agent instance with identifier" << identifier << "does not exist";
        return;
    }

    if (instance->hasAgentInterface()) {
        instance->cleanup();
    } else {
        qCWarning(AKONADICONTROL_LOG) << "Agent instance" << identifier << "has no interface!";
    }

    mAgentInstances.remove(identifier);

    save();

    org::freedesktop::Akonadi::ResourceManager resmanager(Akonadi::DBus::serviceName(Akonadi::DBus::Server),
                                                          QStringLiteral("/ResourceManager"),
                                                          QDBusConnection::sessionBus(),
                                                          this);
    resmanager.removeResourceInstance(instance->identifier());

    // Kill the preprocessor instance, if any.
    org::freedesktop::Akonadi::PreprocessorManager preProcessorManager(Akonadi::DBus::serviceName(Akonadi::DBus::Server),
                                                                       QStringLiteral("/PreprocessorManager"),
                                                                       QDBusConnection::sessionBus(),
                                                                       this);

    preProcessorManager.unregisterInstance(instance->identifier());

    if (instance->hasAgentInterface()) {
        qCDebug(AKONADICONTROL_LOG) << "AgentManager::removeAgentInstance: calling instance->quit()";
        instance->quit();
    } else {
        qCWarning(AKONADICONTROL_LOG) << "Agent instance" << identifier << "has no interface!";
    }

    Q_EMIT agentInstanceRemoved(identifier);
}

QString AgentManager::agentInstanceType(const QString &identifier)
{
    const AgentInstance::Ptr agent = mAgentInstances.value(identifier);
    if (!agent) {
        qCWarning(AKONADICONTROL_LOG) << "Agent instance with identifier" << identifier << "does not exist";
        return QString();
    }

    return agent->agentType();
}

QStringList AgentManager::agentInstances() const
{
    return mAgentInstances.keys();
}

int AgentManager::agentInstanceStatus(const QString &identifier) const
{
    if (!checkInstance(identifier)) {
        return 2;
    }

    return mAgentInstances.value(identifier)->status();
}

QString AgentManager::agentInstanceStatusMessage(const QString &identifier) const
{
    if (!checkInstance(identifier)) {
        return QString();
    }

    return mAgentInstances.value(identifier)->statusMessage();
}

uint AgentManager::agentInstanceProgress(const QString &identifier) const
{
    if (!checkInstance(identifier)) {
        return 0;
    }

    return mAgentInstances.value(identifier)->progress();
}

QString AgentManager::agentInstanceProgressMessage(const QString &identifier) const
{
    Q_UNUSED(identifier)

    return QString();
}

void AgentManager::agentInstanceConfigure(const QString &identifier, qlonglong windowId)
{
    if (!checkAgentInterfaces(identifier, QStringLiteral("agentInstanceConfigure"))) {
        return;
    }

    mAgentInstances.value(identifier)->configure(windowId);
}

bool AgentManager::agentInstanceOnline(const QString &identifier)
{
    if (!checkInstance(identifier)) {
        return false;
    }

    return mAgentInstances.value(identifier)->isOnline();
}

void AgentManager::setAgentInstanceOnline(const QString &identifier, bool state)
{
    if (!checkAgentInterfaces(identifier, QStringLiteral("setAgentInstanceOnline"))) {
        return;
    }

    mAgentInstances.value(identifier)->statusInterface()->setOnline(state);
}

void AgentManager::setAgentInstanceActivities(const QString &identifier, const QStringList &activities)
{
    if (!checkAgentInterfaces(identifier, QStringLiteral("setAgentInstanceActivities"))) {
        return;
    }

    mAgentInstances.value(identifier)->resourceInterface()->setActivities(activities);
}

QStringList AgentManager::agentInstanceActivities(const QString &identifier)
{
    if (!checkAgentInterfaces(identifier, QStringLiteral("agentInstanceActivities"))) {
        return {};
    }
    if (!mAgentInstances.value(identifier)->resourceInterface()) {
        return {};
    }

    return mAgentInstances.value(identifier)->resourceInterface()->activities();
}

void AgentManager::setAgentInstanceActivitiesEnabled(const QString &identifier, bool enabled)
{
    if (!checkAgentInterfaces(identifier, QStringLiteral("setAgentInstanceActivitiesEnabled"))) {
        return;
    }

    mAgentInstances.value(identifier)->resourceInterface()->setActivitiesEnabled(enabled);
}

bool AgentManager::agentInstanceActivitiesEnabled(const QString &identifier)
{
    if (!checkAgentInterfaces(identifier, QStringLiteral("agentInstanceActivitiesEnabled"))) {
        return false;
    }
    if (!mAgentInstances.value(identifier)->resourceInterface()) {
        return false;
    }

    return mAgentInstances.value(identifier)->resourceInterface()->activitiesEnabled();
}

QString AgentManager::agentInstanceAccountId(const QString &identifier)
{
    if (!checkInstance(identifier)) {
        return QString();
    }

    if (!mAgentInstances.value(identifier)->hasAccountInterface()) {
        qCWarning(AKONADICONTROL_LOG) << "Agent instance (agentInstanceAccountId) " << identifier << " has no agent account interface.";
        return QString();
    }

    return mAgentInstances.value(identifier)->accountId();
}

// resource specific methods //
void AgentManager::setAgentInstanceName(const QString &identifier, const QString &name)
{
    if (!checkResourceInterface(identifier, QStringLiteral("setAgentInstanceName"))) {
        return;
    }

    mAgentInstances.value(identifier)->resourceInterface()->setName(name);
}

QString AgentManager::agentInstanceName(const QString &identifier) const
{
    if (!checkInstance(identifier)) {
        return QString();
    }

    const AgentInstance::Ptr instance = mAgentInstances.value(identifier);
    if (!instance->resourceName().isEmpty()) {
        return instance->resourceName();
    }

    if (!checkAgentExists(instance->agentType())) {
        return QString();
    }

    return mAgents.value(instance->agentType()).name;
}

void AgentManager::agentInstanceSynchronize(const QString &identifier)
{
    if (!checkResourceInterface(identifier, QStringLiteral("agentInstanceSynchronize"))) {
        return;
    }

    mAgentInstances.value(identifier)->resourceInterface()->synchronize();
}

void AgentManager::agentInstanceSynchronizeCollectionTree(const QString &identifier)
{
    if (!checkResourceInterface(identifier, QStringLiteral("agentInstanceSynchronizeCollectionTree"))) {
        return;
    }

    mAgentInstances.value(identifier)->resourceInterface()->synchronizeCollectionTree();
}

void AgentManager::agentInstanceSynchronizeCollection(const QString &identifier, qint64 collection)
{
    agentInstanceSynchronizeCollection(identifier, collection, false);
}

void AgentManager::agentInstanceSynchronizeCollection(const QString &identifier, qint64 collection, bool recursive)
{
    if (!checkResourceInterface(identifier, QStringLiteral("agentInstanceSynchronizeCollection"))) {
        return;
    }

    mAgentInstances.value(identifier)->resourceInterface()->synchronizeCollection(collection, recursive);
}

void AgentManager::agentInstanceSynchronizeTags(const QString &identifier)
{
    if (!checkResourceInterface(identifier, QStringLiteral("agentInstanceSynchronizeTags"))) {
        return;
    }

    mAgentInstances.value(identifier)->resourceInterface()->synchronizeTags();
}

void AgentManager::restartAgentInstance(const QString &identifier)
{
    if (!checkInstance(identifier)) {
        return;
    }

    mAgentInstances.value(identifier)->restartWhenIdle();
}

void AgentManager::updatePluginInfos()
{
    const QHash<QString, AgentType> oldInfos = mAgents;
    readPluginInfos();

    for (const AgentType &oldInfo : oldInfos) {
        if (!mAgents.contains(oldInfo.identifier)) {
            Q_EMIT agentTypeRemoved(oldInfo.identifier);
        }
    }

    for (const AgentType &newInfo : std::as_const(mAgents)) {
        if (!oldInfos.contains(newInfo.identifier)) {
            Q_EMIT agentTypeAdded(newInfo.identifier);
            ensureAutoStart(newInfo);
        }
    }
}

void AgentManager::readPluginInfos()
{
    mAgents.clear();

    const QStringList pathList = pluginInfoPathList();

    for (const QString &path : pathList) {
        const QDir directory(path, QStringLiteral("*.desktop"));
        readPluginInfos(directory);
    }
}

void AgentManager::readPluginInfos(const QDir &directory)
{
    const QStringList files = directory.entryList();
    qCDebug(AKONADICONTROL_LOG) << "PLUGINS: " << directory.canonicalPath();
    qCDebug(AKONADICONTROL_LOG) << "PLUGINS: " << files;

    for (int i = 0; i < files.count(); ++i) {
        const QString fileName = directory.absoluteFilePath(files[i]);

        AgentType agentInfo;
        if (agentInfo.load(fileName, this)) {
            if (mAgents.contains(agentInfo.identifier)) {
                qCWarning(AKONADICONTROL_LOG) << "Duplicated agent identifier" << agentInfo.identifier << "from file" << fileName;
                continue;
            }

            const QString disableAutostart = akGetEnv("AKONADI_DISABLE_AGENT_AUTOSTART");
            if (!disableAutostart.isEmpty()) {
                qCDebug(AKONADICONTROL_LOG) << "Autostarting of agents is disabled.";
                agentInfo.capabilities.removeOne(AgentType::CapabilityAutostart);
            }

            if (!mAgentServerEnabled && agentInfo.launchMethod == AgentType::Server) {
                agentInfo.launchMethod = AgentType::Launcher;
            }

            if (agentInfo.launchMethod == AgentType::Process) {
                const QString executable = Akonadi::StandardDirs::findExecutable(agentInfo.exec);
                if (executable.isEmpty()) {
                    qCWarning(AKONADICONTROL_LOG) << "Executable" << agentInfo.exec << "for agent" << agentInfo.identifier << "could not be found!";
                    continue;
                }
            }

            qCDebug(AKONADICONTROL_LOG) << "PLUGINS inserting: " << agentInfo.identifier << agentInfo.instanceCounter << agentInfo.capabilities;
            mAgents.insert(agentInfo.identifier, agentInfo);
        }
    }
}

QStringList AgentManager::pluginInfoPathList()
{
    return Akonadi::StandardDirs::locateAllResourceDirs(QStringLiteral("akonadi/agents"));
}

void AgentManager::load()
{
    org::freedesktop::Akonadi::ResourceManager resmanager(Akonadi::DBus::serviceName(Akonadi::DBus::Server),
                                                          QStringLiteral("/ResourceManager"),
                                                          QDBusConnection::sessionBus(),
                                                          this);
    const QStringList knownResources = resmanager.resourceInstances();

    QSettings file(Akonadi::StandardDirs::agentsConfigFile(Akonadi::StandardDirs::ReadOnly), QSettings::IniFormat);
    file.beginGroup(QStringLiteral("Instances"));
    const QStringList entries = file.childGroups();
    for (int i = 0; i < entries.count(); ++i) {
        const QString instanceIdentifier = entries[i];

        if (mAgentInstances.contains(instanceIdentifier)) {
            qCWarning(AKONADICONTROL_LOG) << "Duplicated instance identifier" << instanceIdentifier << "found in agentsrc";
            continue;
        }

        file.beginGroup(entries[i]);

        const QString agentType = file.value(QStringLiteral("AgentType")).toString();
        const auto typeIter = mAgents.constFind(agentType);
        if (typeIter == mAgents.cend() || typeIter->exec.isEmpty()) {
            // Check whether this is one of the obsolete agent types and just remove it.
            if (std::find(std::begin(retiredAgentTypes), std::end(retiredAgentTypes), agentType) != std::end(retiredAgentTypes)) {
                qCInfo(AKONADICONTROL_LOG) << agentType << "has been retired in a previous version, cleaning up " << entries[i];
                file.endGroup();
                file.remove(entries[i]);
                // This will take care of removing all related data from Akonadi database
                resmanager.removeResourceInstance(entries[i]);
                continue;
            }

            qCWarning(AKONADICONTROL_LOG) << "Reference to unknown agent type" << agentType << "in agentsrc, creating a fake entry.";
            if (typeIter == mAgents.cend()) {
                AgentType type;
                type.identifier = type.name = agentType;
                mAgents.insert(type.identifier, type);
            }

            auto brokenInstance = AgentInstance::Ptr{new Akonadi::AgentBrokenInstance{agentType, *this}};
            brokenInstance->setIdentifier(instanceIdentifier);
            mAgentInstances.insert(instanceIdentifier, brokenInstance);
            file.endGroup();
            continue;
        }

        const AgentType &type = *typeIter;

        // recover if the db has been deleted in the meantime or got otherwise corrupted
        if (!knownResources.contains(instanceIdentifier) && type.capabilities.contains(AgentType::CapabilityResource)) {
            qCDebug(AKONADICONTROL_LOG) << "Recovering instance" << instanceIdentifier << "after database loss";
            registerAgentAtServer(instanceIdentifier, type);
        }

        const AgentInstance::Ptr instance = createAgentInstance(type);
        instance->setIdentifier(instanceIdentifier);
        if (instance->start(type)) {
            mAgentInstances.insert(instanceIdentifier, instance);
        }

        file.endGroup();
    }

    file.endGroup();
}

void AgentManager::save()
{
    QSettings file(Akonadi::StandardDirs::agentsConfigFile(Akonadi::StandardDirs::WriteOnly), QSettings::IniFormat);

    for (const AgentType &info : std::as_const(mAgents)) {
        info.save(&file);
    }

    file.beginGroup(QStringLiteral("Instances"));
    file.remove(QString());
    for (const AgentInstance::Ptr &instance : std::as_const(mAgentInstances)) {
        file.beginGroup(instance->identifier());
        file.setValue(QStringLiteral("AgentType"), instance->agentType());
        file.endGroup();
    }

    file.endGroup();
}

void AgentManager::setupSingleShotConnection(const AgentInstance::Ptr &instance)
{
    auto iface = instance->controlInterface();
    if (!iface) {
        qCWarning(AKONADICONTROL_LOG) << "SingleShot agent" << instance->identifier() << "has no status interface.";
        return;
    }

    QObject::connect(iface, &org::freedesktop::Akonadi::Agent::Control::finished, instance.data(), [=]() {
        qCDebug(AKONADICONTROL_LOG) << "SingleShot agent" << instance->identifier() << "signaled finished(), shutting down.";
        instance->quit();
    });
}

void AgentManager::serviceOwnerChanged(const QString &name, const QString &oldOwner, const QString &newOwner)
{
    Q_UNUSED(oldOwner)
    // This is called by the D-Bus server when a service comes up, goes down or changes ownership for some reason
    // and this is where we "hook up" our different Agent interfaces.

    // Ignore DBus address name (e.g. :1.310)
    if (name.startsWith(u':')) {
        return;
    }

    // Ignore services belonging to another Akonadi instance
    const auto parsedInstance = Akonadi::DBus::parseInstanceIdentifier(name);
    const auto currentInstance = Akonadi::Instance::hasIdentifier() ? std::optional<QString>(Akonadi::Instance::identifier()) : std::nullopt;
    if (parsedInstance != currentInstance) {
        return;
    }

    qCDebug(AKONADICONTROL_LOG) << "Service" << name << "owner changed from" << oldOwner << "to" << newOwner;

    const QString akServerName = Akonadi::DBus::serviceName(Akonadi::DBus::Server);
    const QString akAgentServerName = Akonadi::DBus::serviceName(Akonadi::DBus::AgentServer);
    if ((name == akServerName || name == akAgentServerName) && !newOwner.isEmpty()) {
        if (QDBusConnection::sessionBus().interface()->isServiceRegistered(akServerName)
            && (!mAgentServer || QDBusConnection::sessionBus().interface()->isServiceRegistered(akAgentServerName))) {
            // server is operational, start agents
            continueStartup();
        }
    }

    const auto service = Akonadi::DBus::parseAgentServiceName(name);
    if (!service.has_value()) {
        return;
    }
    switch (service->agentType) {
    case Akonadi::DBus::Agent: {
        // An agent service went up or down
        if (newOwner.isEmpty()) {
            return; // It went down: we don't care here.
        }

        if (!mAgentInstances.contains(service->identifier)) {
            return;
        }

        const AgentInstance::Ptr instance = mAgentInstances.value(service->identifier);
        const bool restarting = instance->hasAgentInterface();
        if (!instance->obtainAgentInterface()) {
            return;
        }

        Q_ASSERT(mAgents.contains(instance->agentType()));
        const bool isResource = mAgents.value(instance->agentType()).capabilities.contains(AgentType::CapabilityResource);

        if (!restarting && (!isResource || instance->hasResourceInterface())) {
            Q_EMIT agentInstanceAdded(service->identifier);
        }

        if (mAgents.value(instance->agentType()).capabilities.contains(AgentType::CapabilitySingleShot)) {
            setupSingleShotConnection(instance);
        }

        break;
    }
    case Akonadi::DBus::Resource: {
        // A resource service went up or down
        if (newOwner.isEmpty()) {
            return; // It went down: we don't care here.
        }

        if (!mAgentInstances.contains(service->identifier)) {
            return;
        }

        const AgentInstance::Ptr instance = mAgentInstances.value(service->identifier);
        const bool restarting = instance->hasResourceInterface();
        if (!instance->obtainResourceInterface()) {
            return;
        }

        if (!restarting && instance->hasAgentInterface()) {
            Q_EMIT agentInstanceAdded(service->identifier);
        }

        break;
    }
    case Akonadi::DBus::Preprocessor: {
        // A preprocessor service went up or down

        // If the preprocessor is going up then the org.freedesktop.Akonadi.Agent.* interface
        // should be already up (as it's registered before the preprocessor one).
        // So if we don't know about the preprocessor as agent instance
        // then it's not our preprocessor.

        // If the preprocessor is going down then either the agent interface already
        // went down (and it has been already unregistered on the manager side)
        // or it's still registered as agent and WE have to unregister it.
        // The order of interface deletions depends on Qt but we handle both cases.

        // Check if we "know" about it.
        qCDebug(AKONADICONTROL_LOG) << "Preprocessor " << service->identifier << " is going up or down...";

        if (!mAgentInstances.contains(service->identifier)) {
            qCDebug(AKONADICONTROL_LOG) << "But it isn't registered as agent... not mine (anymore?)";
            return; // not our agent (?)
        }

        org::freedesktop::Akonadi::PreprocessorManager preProcessorManager(Akonadi::DBus::serviceName(Akonadi::DBus::Server),
                                                                           QStringLiteral("/PreprocessorManager"),
                                                                           QDBusConnection::sessionBus(),
                                                                           this);

        if (!preProcessorManager.isValid()) {
            qCWarning(AKONADICONTROL_LOG) << "Could not connect to PreprocessorManager via D-Bus:" << preProcessorManager.lastError().message();
        } else {
            if (newOwner.isEmpty()) {
                // The preprocessor went down. Unregister it on server side.

                preProcessorManager.unregisterInstance(service->identifier);

            } else {
                // The preprocessor went up. Register it on server side.

                if (!mAgentInstances.value(service->identifier)->obtainPreprocessorInterface()) {
                    // Hm.. couldn't hook up its preprocessor interface..
                    // Make sure we don't have it in the preprocessor chain
                    qCWarning(AKONADICONTROL_LOG) << "Couldn't obtain preprocessor interface for instance" << service->identifier;

                    preProcessorManager.unregisterInstance(service->identifier);
                    return;
                }

                qCDebug(AKONADICONTROL_LOG) << "Registering preprocessor instance" << service->identifier;

                // Add to the preprocessor chain
                preProcessorManager.registerInstance(service->identifier);
            }
        }

        break;
    }
    default:
        break;
    }

    registerServiceIfDoneWaitingForAgents();
}

void AgentManager::registerServiceIfDoneWaitingForAgents()
{
    if (mAllAgentsStarted && !mAllAgentsRegisteredOnDBus) {
        bool allRegistered = true;
        for (const AgentInstance::Ptr &agent : std::as_const(mAgentInstances)) {
            // Also consider "statically broken" agents as registered, which really means: don't wait for them.
            // Looking at agent status() would open a can of worms because that one can theoretically change at any time.
            // I think such agents would also report their broken status via DBus, so they'd be registered and therefore
            // be covered by the first condition anyway?
            // Also don't expect non-autostarting agents to start.
            if (!agent->dbusServiceRegistered() && !qobject_cast<Akonadi::AgentBrokenInstance *>(agent)
                && mAgents.value(agent->agentType()).capabilities.contains(AgentType::CapabilityAutostart)) {
                allRegistered = false;
                break;
            }
        }

        mAllAgentsRegisteredOnDBus = allRegistered;
        if (allRegistered) {
            // register the real service name once everything is up and running
            if (!QDBusConnection::sessionBus().registerService(Akonadi::DBus::serviceName(Akonadi::DBus::Control))) {
                // besides a race with an older Akonadi server I have no idea how we could possibly get here...
                qFatal("Unable to register service as %s despite having the lock. Error was: %s",
                       qPrintable(Akonadi::DBus::serviceName(Akonadi::DBus::Control)),
                       qPrintable(QDBusConnection::sessionBus().lastError().message()));
            }
            qCInfo(AKONADICONTROL_LOG) << "Akonadi server is now operational.";
        }
    }
}

bool AgentManager::checkInstance(const QString &identifier) const
{
    if (!mAgentInstances.contains(identifier)) {
        qCWarning(AKONADICONTROL_LOG) << "Agent instance with identifier " << identifier << " does not exist";
        return false;
    }

    return true;
}

bool AgentManager::checkResourceInterface(const QString &identifier, const QString &method) const
{
    if (!checkInstance(identifier)) {
        return false;
    }

    if (!mAgents[mAgentInstances[identifier]->agentType()].capabilities.contains(QLatin1StringView("Resource"))) {
        return false;
    }

    if (!mAgentInstances[identifier]->hasResourceInterface()) {
        qCWarning(AKONADICONTROL_LOG) << QLatin1StringView("AgentManager::") + method << " Agent instance " << identifier << " has no resource interface!";
        return false;
    }

    return true;
}

bool AgentManager::checkAgentExists(const QString &identifier) const
{
    if (!mAgents.contains(identifier)) {
        qCWarning(AKONADICONTROL_LOG) << "Agent instance " << identifier << " does not exist.";
        return false;
    }

    return true;
}

bool AgentManager::checkAgentInterfaces(const QString &identifier, const QString &method) const
{
    if (!checkInstance(identifier)) {
        return false;
    }

    if (!mAgentInstances.value(identifier)->hasAgentInterface()) {
        qCWarning(AKONADICONTROL_LOG) << "Agent instance (" << method << ") " << identifier << " has no agent interface.";
        return false;
    }

    return true;
}

void AgentManager::ensureAutoStart(const AgentType &info)
{
    if (!info.capabilities.contains(AgentType::CapabilityAutostart)) {
        return; // no an autostart agent
    }

    if (info.capabilities.contains(AgentType::CapabilitySingleShot)) {
        if (mRanSingleShotAgents.contains(info.identifier)) {
            qCDebug(AKONADICONTROL_LOG) << "Skipping SingleShot agent" << info.identifier << "because it already ran.";
            return;
        }
    }

    org::freedesktop::Akonadi::AgentServer agentServer(Akonadi::DBus::serviceName(Akonadi::DBus::AgentServer),
                                                       QStringLiteral("/AgentServer"),
                                                       QDBusConnection::sessionBus(),
                                                       this);

    if (mAgentInstances.contains(info.identifier) || (agentServer.isValid() && agentServer.started(info.identifier))) {
        return; // already running
    }

    const AgentInstance::Ptr instance = createAgentInstance(info);
    instance->setIdentifier(info.identifier);
    if (instance->start(info)) {
        mAgentInstances.insert(instance->identifier(), instance);
        registerAgentAtServer(instance->identifier(), info);
        save();

        if (info.capabilities.contains(AgentType::CapabilitySingleShot)) {
            setupSingleShotConnection(instance);
            mRanSingleShotAgents.insert(info.identifier);
        }
    }
}

void AgentManager::agentExeChanged(const QString &fileName)
{
    if (!QFile::exists(fileName)) {
        return;
    }

    for (const AgentType &type : std::as_const(mAgents)) {
        if (fileName.endsWith(type.exec)) {
            for (const AgentInstance::Ptr &instance : std::as_const(mAgentInstances)) {
                if (instance->agentType() == type.identifier) {
                    instance->restartWhenIdle();
                }
            }
        }
    }
}

void AgentManager::registerAgentAtServer(const QString &agentIdentifier, const AgentType &type)
{
    if (type.capabilities.contains(AgentType::CapabilityResource)) {
        QScopedPointer<org::freedesktop::Akonadi::ResourceManager> resmanager(
            new org::freedesktop::Akonadi::ResourceManager(Akonadi::DBus::serviceName(Akonadi::DBus::Server),
                                                           QStringLiteral("/ResourceManager"),
                                                           QDBusConnection::sessionBus(),
                                                           this));
        resmanager->addResourceInstance(agentIdentifier, type.capabilities);
    }
}

void AgentManager::addSearch(const QString &query, const QString &queryLanguage, qint64 resultCollectionId)
{
    qCDebug(AKONADICONTROL_LOG) << "AgentManager::addSearch" << query << queryLanguage << resultCollectionId;
    for (const AgentInstance::Ptr &instance : std::as_const(mAgentInstances)) {
        const AgentType type = mAgents.value(instance->agentType());
        if (type.capabilities.contains(AgentType::CapabilitySearch) && instance->searchInterface()) {
            instance->searchInterface()->addSearch(query, queryLanguage, resultCollectionId);
        }
    }
}

void AgentManager::removeSearch(quint64 resultCollectionId)
{
    qCDebug(AKONADICONTROL_LOG) << "AgentManager::removeSearch" << resultCollectionId;
    for (const AgentInstance::Ptr &instance : std::as_const(mAgentInstances)) {
        const AgentType type = mAgents.value(instance->agentType());
        if (type.capabilities.contains(AgentType::CapabilitySearch) && instance->searchInterface()) {
            instance->searchInterface()->removeSearch(resultCollectionId);
        }
    }
}

#include "agentmanager.moc"

#include "moc_agentmanager.cpp"
