#include "ComponentUpdateTask.h"
#include <algorithm>

#include "Component.h"
#include "ComponentUpdateTask_p.h"
#include "PackProfile.h"
#include "PackProfile_p.h"
#include "ProblemProvider.h"
#include "Version.h"
#include "cassert"
#include "meta/Index.h"
#include "meta/Version.h"
#include "minecraft/MinecraftInstance.h"
#include "minecraft/OneSixVersionFormat.h"
#include "minecraft/ProfileUtils.h"
#include "net/Mode.h"

#include "Application.h"
#include "tasks/Task.h"

#include "minecraft/Logging.h"

/*
 * This is responsible for loading the components of a component list AND resolving dependency issues between them
 */

/*
 * FIXME: the 'one shot async task' nature of this does not fit the intended usage
 * Really, it should be a reactor/state machine that receives input from the application
 * and dynamically adapts to changing requirements...
 *
 * The reactor should be the only entry into manipulating the PackProfile.
 * See: https://en.wikipedia.org/wiki/Reactor_pattern
 */

/*
 * Or make this operate on a snapshot of the PackProfile state, then merge results in as long as the snapshot and PackProfile didn't change?
 * If the component list changes, start over.
 */

/*
 * TODO: This task launches multiple other tasks. As such it should be converted to a ConcurrentTask
 */
ComponentUpdateTask::ComponentUpdateTask(Mode mode, Net::Mode netmode, PackProfile* list) : Task()
{
    d.reset(new ComponentUpdateTaskData);
    d->m_profile = list;
    d->mode = mode;
    d->netmode = netmode;
}

ComponentUpdateTask::~ComponentUpdateTask() {}

bool ComponentUpdateTask::canAbort() const
{
    for (const auto& status : d->remoteLoadStatusList) {
        if (status.task && !status.task->canAbort()) {
            return false;
        }
    }

    return true;
}

bool ComponentUpdateTask::abort()
{
    bool aborted = true;
    for (const auto& status : d->remoteLoadStatusList) {
        if (status.task && !status.task->abort()) {
            aborted = false;
        }
    }

    return aborted;
}

Net::Mode ComponentUpdateTask::netMode()
{
    return d->netmode;
}

void ComponentUpdateTask::executeTask()
{
    qCDebug(instanceProfileResolveC) << "Loading components";
    setStatus(tr("Loading components"));
    loadComponents();
}

namespace {
enum class LoadResult { LoadedLocal, RequiresRemote, Failed };

LoadResult composeLoadResult(LoadResult a, LoadResult b)
{
    if (a < b) {
        return b;
    }
    return a;
}

static LoadResult loadComponent(ComponentPtr component, Task::Ptr& loadTask, Net::Mode netmode)
{
    if (component->m_loaded) {
        qCDebug(instanceProfileResolveC) << component->getName() << "is already loaded";
        return LoadResult::LoadedLocal;
    }

    LoadResult result = LoadResult::Failed;
    auto customPatchFilename = component->getFilename();
    if (QFile::exists(customPatchFilename)) {
        // if local file exists...

        // check for uid problems inside...
        bool fileChanged = false;
        auto file = ProfileUtils::parseJsonFile(QFileInfo(customPatchFilename), false);
        if (file->uid != component->m_uid) {
            file->uid = component->m_uid;
            fileChanged = true;
        }
        if (fileChanged) {
            // FIXME: @QUALITY do not ignore return value
            ProfileUtils::saveJsonFile(OneSixVersionFormat::versionFileToJson(file), customPatchFilename);
        }

        component->m_file = file;
        component->m_loaded = true;
        result = LoadResult::LoadedLocal;
    } else {
        auto metaVersion = APPLICATION->metadataIndex()->get(component->m_uid, component->m_version);
        component->m_metaVersion = metaVersion;
        if (metaVersion->isLoaded()) {
            component->m_loaded = true;
            result = LoadResult::LoadedLocal;
        } else {
            loadTask = APPLICATION->metadataIndex()->loadVersion(component->m_uid, component->m_version, netmode);
            loadTask->start();
            if (netmode == Net::Mode::Online)
                result = LoadResult::RequiresRemote;
            else if (metaVersion->isLoaded())
                result = LoadResult::LoadedLocal;
            else
                result = LoadResult::Failed;
        }
    }
    return result;
}

// FIXME: dead code. determine if this can still be useful?
/*
static LoadResult loadPackProfile(ComponentPtr component, Task::Ptr& loadTask, Net::Mode netmode)
{
    if(component->m_loaded)
    {
        qDebug() << component->getName() << "is already loaded";
        return LoadResult::LoadedLocal;
    }

    LoadResult result = LoadResult::Failed;
    auto metaList = APPLICATION->metadataIndex()->get(component->m_uid);
    if(metaList->isLoaded())
    {
        component->m_loaded = true;
        result = LoadResult::LoadedLocal;
    }
    else
    {
        metaList->load(netmode);
        loadTask = metaList->getCurrentTask();
        result = LoadResult::RequiresRemote;
    }
    return result;
}
*/

}  // namespace

void ComponentUpdateTask::loadComponents()
{
    LoadResult result = LoadResult::LoadedLocal;
    size_t taskIndex = 0;
    size_t componentIndex = 0;
    d->remoteLoadSuccessful = true;

    // load all the components OR their lists...
    for (auto component : d->m_profile->d->components) {
        Task::Ptr loadTask;
        LoadResult singleResult;
        RemoteLoadStatus::Type loadType;
        component->resetComponentProblems();
        // FIXME: to do this right, we need to load the lists and decide on which versions to use during dependency resolution. For now,
        // ignore all that...
#if 0
        switch(d->mode)
        {
            case Mode::Launch:
            {
                singleResult = loadComponent(component, loadTask, d->netmode);
                loadType = RemoteLoadStatus::Type::Version;
                break;
            }
            case Mode::Resolution:
            {
                singleResult = loadPackProfile(component, loadTask, d->netmode);
                loadType = RemoteLoadStatus::Type::List;
                break;
            }
        }
#else
        singleResult = loadComponent(component, loadTask, d->netmode);
        loadType = RemoteLoadStatus::Type::Version;
#endif
        if (singleResult == LoadResult::LoadedLocal) {
            component->updateCachedData();
        }
        result = composeLoadResult(result, singleResult);
        if (loadTask) {
            qCDebug(instanceProfileResolveC) << d->m_profile->d->m_instance->name() << "|"
                                             << "Remote loading is being run for" << component->getName();
            connect(loadTask.get(), &Task::succeeded, this, [this, taskIndex]() { remoteLoadSucceeded(taskIndex); });
            connect(loadTask.get(), &Task::failed, this, [this, taskIndex](const QString& error) { remoteLoadFailed(taskIndex, error); });
            connect(loadTask.get(), &Task::aborted, this, [this, taskIndex]() { remoteLoadFailed(taskIndex, tr("Aborted")); });
            RemoteLoadStatus status;
            status.type = loadType;
            status.PackProfileIndex = componentIndex;
            status.task = loadTask;
            d->remoteLoadStatusList.append(status);
            taskIndex++;
        }
        componentIndex++;
    }
    d->remoteTasksInProgress = taskIndex;
    m_progressTotal = static_cast<int>(taskIndex);
    switch (result) {
        case LoadResult::LoadedLocal: {
            // Everything got loaded. Advance to dependency resolution.
            performUpdateActions();
            resolveDependencies(d->mode == Mode::Launch || d->netmode == Net::Mode::Offline);
            return;
        }
        case LoadResult::RequiresRemote: {
            // we wait for signals.
            break;
        }
        case LoadResult::Failed: {
            emitFailed(tr("Some component metadata load tasks failed."));
            return;
        }
    }

    setDetails(tr("Downloading metadata for %1 components").arg(taskIndex));
}

namespace {
struct RequireEx : public Meta::Require {
    size_t indexOfFirstDependee = 0;
};
struct RequireCompositionResult {
    bool ok;
    RequireEx outcome;
};
using RequireExSet = std::set<RequireEx>;
}  // namespace

static RequireCompositionResult composeRequirement(const RequireEx& a, const RequireEx& b)
{
    assert(a.uid == b.uid);
    RequireEx out;
    out.uid = a.uid;
    out.indexOfFirstDependee = std::min(a.indexOfFirstDependee, b.indexOfFirstDependee);
    if (a.equalsVersion.isEmpty()) {
        out.equalsVersion = b.equalsVersion;
    } else if (b.equalsVersion.isEmpty()) {
        out.equalsVersion = a.equalsVersion;
    } else if (a.equalsVersion == b.equalsVersion) {
        out.equalsVersion = a.equalsVersion;
    } else {
        // FIXME: mark error as explicit version conflict
        return { false, out };
    }

    if (a.suggests.isEmpty()) {
        out.suggests = b.suggests;
    } else if (b.suggests.isEmpty()) {
        out.suggests = a.suggests;
    } else {
        Version aVer(a.suggests);
        Version bVer(b.suggests);
        out.suggests = (aVer < bVer ? b.suggests : a.suggests);
    }
    return { true, out };
}

// gather the requirements from all components, finding any obvious conflicts
static bool gatherRequirementsFromComponents(const ComponentContainer& input, RequireExSet& output)
{
    bool succeeded = true;
    size_t componentNum = 0;
    for (auto component : input) {
        auto& componentRequires = component->m_cachedRequires;
        for (const auto& componentRequire : componentRequires) {
            auto found = std::find_if(output.cbegin(), output.cend(),
                                      [componentRequire](const Meta::Require& req) { return req.uid == componentRequire.uid; });

            RequireEx componenRequireEx;
            componenRequireEx.uid = componentRequire.uid;
            componenRequireEx.suggests = componentRequire.suggests;
            componenRequireEx.equalsVersion = componentRequire.equalsVersion;
            componenRequireEx.indexOfFirstDependee = componentNum;

            if (found != output.cend()) {
                // found... process it further
                auto result = composeRequirement(componenRequireEx, *found);
                if (result.ok) {
                    output.erase(componenRequireEx);
                    output.insert(result.outcome);
                } else {
                    qCCritical(instanceProfileResolveC) << "Conflicting requirements:" << componentRequire.uid
                                                        << "versions:" << componentRequire.equalsVersion << ";" << (*found).equalsVersion;
                }
                succeeded &= result.ok;
            } else {
                // not found, accumulate
                output.insert(componenRequireEx);
            }
        }
        componentNum++;
    }
    return succeeded;
}

/// Get list of uids that can be trivially removed because nothing is depending on them anymore (and they are installed as deps)
static void getTrivialRemovals(const ComponentContainer& components, const RequireExSet& reqs, QStringList& toRemove)
{
    for (const auto& component : components) {
        if (!component->m_dependencyOnly)
            continue;
        if (!component->m_cachedVolatile)
            continue;
        RequireEx reqNeedle;
        reqNeedle.uid = component->m_uid;
        const auto iter = reqs.find(reqNeedle);
        if (iter == reqs.cend()) {
            toRemove.append(component->m_uid);
        }
    }
}

/**
 * handles:
 * - trivial addition (there is an unmet requirement and it can be trivially met by adding something)
 * - trivial version conflict of dependencies == explicit version required and installed is different
 *
 * toAdd - set of requirements than mean adding a new component
 * toChange - set of requirements that mean changing version of an existing component
 */
static bool getTrivialComponentChanges(const ComponentIndex& index, const RequireExSet& input, RequireExSet& toAdd, RequireExSet& toChange)
{
    enum class Decision { Undetermined, Met, Missing, VersionNotSame, LockedVersionNotSame } decision = Decision::Undetermined;

    QString reqStr;
    bool succeeded = true;
    // list the composed requirements and say if they are met or unmet
    for (auto& req : input) {
        do {
            if (req.equalsVersion.isEmpty()) {
                reqStr = QString("Req: %1").arg(req.uid);
                if (index.contains(req.uid)) {
                    decision = Decision::Met;
                } else {
                    toAdd.insert(req);
                    decision = Decision::Missing;
                }
                break;
            } else {
                reqStr = QString("Req: %1 == %2").arg(req.uid, req.equalsVersion);
                const auto& compIter = index.find(req.uid);
                if (compIter == index.cend()) {
                    toAdd.insert(req);
                    decision = Decision::Missing;
                    break;
                }
                auto& comp = (*compIter);
                if (comp->getVersion() != req.equalsVersion) {
                    if (comp->isCustom()) {
                        decision = Decision::LockedVersionNotSame;
                    } else {
                        if (comp->m_dependencyOnly) {
                            decision = Decision::VersionNotSame;
                        } else {
                            decision = Decision::LockedVersionNotSame;
                        }
                    }
                    break;
                }
                decision = Decision::Met;
            }
        } while (false);
        switch (decision) {
            case Decision::Undetermined:
                qCCritical(instanceProfileResolveC) << "No decision for" << reqStr;
                succeeded = false;
                break;
            case Decision::Met:
                qCDebug(instanceProfileResolveC) << reqStr << "Is met.";
                break;
            case Decision::Missing:
                qCDebug(instanceProfileResolveC) << reqStr << "Is missing and should be added at" << req.indexOfFirstDependee;
                toAdd.insert(req);
                break;
            case Decision::VersionNotSame:
                qCDebug(instanceProfileResolveC) << reqStr << "already has different version that can be changed.";
                toChange.insert(req);
                break;
            case Decision::LockedVersionNotSame:
                qCDebug(instanceProfileResolveC) << reqStr << "already has different version that cannot be changed.";
                succeeded = false;
                break;
        }
    }
    return succeeded;
}

ComponentContainer ComponentUpdateTask::collectTreeLinked(const QString& uid)
{
    ComponentContainer linked;

    auto& components = d->m_profile->d->components;
    auto& componentIndex = d->m_profile->d->componentIndex;
    auto& instance = d->m_profile->d->m_instance;
    for (auto comp : components) {
        qCDebug(instanceProfileResolveC) << instance->name() << "|"
                                         << "scanning" << comp->getID() << ":" << comp->getVersion() << "for tree link";
        auto dep = std::find_if(comp->m_cachedRequires.cbegin(), comp->m_cachedRequires.cend(),
                                [uid](const Meta::Require& req) -> bool { return req.uid == uid; });
        if (dep != comp->m_cachedRequires.cend()) {
            qCDebug(instanceProfileResolveC) << instance->name() << "|" << comp->getID() << ":" << comp->getVersion() << "depends on"
                                             << uid;
            linked.append(comp);
        }
    }
    auto iter = componentIndex.find(uid);
    if (iter != componentIndex.end()) {
        ComponentPtr comp = *iter;
        comp->updateCachedData();
        qCDebug(instanceProfileResolveC) << instance->name() << "|" << comp->getID() << ":" << comp->getVersion() << "has"
                                         << comp->m_cachedRequires.size() << "dependencies";
        for (auto dep : comp->m_cachedRequires) {
            qCDebug(instanceProfileC) << instance->name() << "|" << uid << "depends on" << dep.uid;
            auto found = componentIndex.find(dep.uid);
            if (found != componentIndex.end()) {
                qCDebug(instanceProfileC) << instance->name() << "|" << (*found)->getID() << "is present";
                linked.append(*found);
            }
        }
    }
    return linked;
}

// FIXME, TODO: decouple dependency resolution from loading
// FIXME: This works directly with the PackProfile internals. It shouldn't! It needs richer data types than PackProfile uses.
// FIXME: throw all this away and use a graph
void ComponentUpdateTask::resolveDependencies(bool checkOnly)
{
    qCDebug(instanceProfileResolveC) << "Resolving dependencies";
    /*
     * this is a naive dependency resolving algorithm. all it does is check for following conditions and react in simple ways:
     * 1. There are conflicting dependencies on the same uid with different exact version numbers
     *    -> hard error
     * 2. A dependency has non-matching exact version number
     *    -> hard error
     * 3. A dependency is entirely missing and needs to be injected before the dependee(s)
     *    -> requirements are injected
     *
     * NOTE: this is a placeholder and should eventually be replaced with something 'serious'
     */
    auto& components = d->m_profile->d->components;
    auto& componentIndex = d->m_profile->d->componentIndex;

    RequireExSet allRequires;
    QStringList toRemove;
    do {
        allRequires.clear();
        toRemove.clear();
        if (!gatherRequirementsFromComponents(components, allRequires)) {
            finalizeComponents();
            emitFailed(tr("Conflicting requirements detected during dependency checking!"));
            return;
        }
        getTrivialRemovals(components, allRequires, toRemove);
        if (!toRemove.isEmpty()) {
            qCDebug(instanceProfileResolveC) << "Removing obsolete components...";
            for (auto& remove : toRemove) {
                qCDebug(instanceProfileResolveC) << "Removing" << remove;
                d->m_profile->remove(remove);
            }
        }
    } while (!toRemove.isEmpty());
    RequireExSet toAdd;
    RequireExSet toChange;
    bool succeeded = getTrivialComponentChanges(componentIndex, allRequires, toAdd, toChange);
    if (!succeeded) {
        finalizeComponents();
        emitFailed(tr("Instance has conflicting dependencies."));
        return;
    }
    if (checkOnly) {
        finalizeComponents();
        if (toAdd.size() || toChange.size()) {
            emitFailed(tr("Instance has unresolved dependencies while loading/checking for launch."));
        } else {
            emitSucceeded();
        }
        return;
    }

    bool recursionNeeded = false;
    if (toAdd.size()) {
        // add stuff...
        for (auto& add : toAdd) {
            auto component = makeShared<Component>(d->m_profile, add.uid);
            if (!add.equalsVersion.isEmpty()) {
                // exact version
                qCDebug(instanceProfileResolveC)
                    << "Adding" << add.uid << "version" << add.equalsVersion << "at position" << add.indexOfFirstDependee;
                component->m_version = add.equalsVersion;
            } else {
                // version needs to be decided
                qCDebug(instanceProfileResolveC) << "Adding" << add.uid << "at position" << add.indexOfFirstDependee;
                // ############################################################################################################
                // HACK HACK HACK HACK FIXME: this is a placeholder for deciding what version to use. For now, it is hardcoded.
                if (!add.suggests.isEmpty()) {
                    component->m_version = add.suggests;
                } else {
                    if (add.uid == "org.lwjgl") {
                        component->m_version = "2.9.1";
                    } else if (add.uid == "org.lwjgl3") {
                        component->m_version = "3.1.2";
                    } else if (add.uid == "net.fabricmc.intermediary" || add.uid == "org.quiltmc.hashed") {
                        auto minecraft = std::find_if(components.begin(), components.end(),
                                                      [](ComponentPtr& cmp) { return cmp->getID() == "net.minecraft"; });
                        if (minecraft != components.end()) {
                            component->m_version = (*minecraft)->getVersion();
                        }
                    }
                }
                // HACK HACK HACK HACK FIXME: this is a placeholder for deciding what version to use. For now, it is hardcoded.
                // ############################################################################################################
            }
            component->m_dependencyOnly = true;
            // FIXME: this should not work directly with the component list
            d->m_profile->insertComponent(add.indexOfFirstDependee, component);
            componentIndex[add.uid] = component;
        }
        recursionNeeded = true;
    }
    if (toChange.size()) {
        // change a version of something that exists
        for (auto& change : toChange) {
            // FIXME: this should not work directly with the component list
            qCDebug(instanceProfileResolveC) << "Setting version of" << change.uid << "to" << change.equalsVersion;
            auto component = componentIndex[change.uid];
            component->setVersion(change.equalsVersion);
        }
        recursionNeeded = true;
    }

    if (recursionNeeded) {
        loadComponents();
    } else {
        finalizeComponents();
        emitSucceeded();
    }
}

// Variant visitation via lambda
template <class... Ts>
struct overload : Ts... {
    using Ts::operator()...;
};
template <class... Ts>
overload(Ts...) -> overload<Ts...>;

void ComponentUpdateTask::performUpdateActions()
{
    auto& instance = d->m_profile->d->m_instance;
    bool addedActions;
    QStringList toRemove;
    do {
        addedActions = false;
        toRemove.clear();
        auto& components = d->m_profile->d->components;
        auto& componentIndex = d->m_profile->d->componentIndex;
        for (auto component : components) {
            if (!component) {
                continue;
            }
            auto action = component->getUpdateAction();
            auto visitor =
                overload{ [](const UpdateActionNone&) {
                             // noop
                         },
                          [&component, &instance](const UpdateActionChangeVersion& cv) {
                              qCDebug(instanceProfileResolveC) << instance->name() << "|"
                                                               << "UpdateActionChangeVersion" << component->getID() << ":"
                                                               << component->getVersion() << "change to" << cv.targetVersion;
                              component->setVersion(cv.targetVersion);
                              component->waitLoadMeta();
                          },
                          [&component, &instance](const UpdateActionLatestRecommendedCompatible& lrc) {
                              qCDebug(instanceProfileResolveC)
                                  << instance->name() << "|"
                                  << "UpdateActionLatestRecommendedCompatible" << component->getID() << ":" << component->getVersion()
                                  << "updating to latest recommend or compatible with" << lrc.parentUid << lrc.version;
                              auto versionList = APPLICATION->metadataIndex()->get(component->getID());
                              if (versionList) {
                                  versionList->waitToLoad();
                                  auto recommended = versionList->getRecommendedForParent(lrc.parentUid, lrc.version);
                                  if (!recommended) {
                                      recommended = versionList->getLatestForParent(lrc.parentUid, lrc.version);
                                  }
                                  if (recommended) {
                                      component->setVersion(recommended->version());
                                      component->waitLoadMeta();
                                      return;
                                  } else {
                                      component->addComponentProblem(ProblemSeverity::Error,
                                                                     QObject::tr("No compatible version of %1 found for %2 %3")
                                                                         .arg(component->getName(), lrc.parentName, lrc.version));
                                  }
                              } else {
                                  component->addComponentProblem(
                                      ProblemSeverity::Error,
                                      QObject::tr("No version list in metadata index for %1").arg(component->getID()));
                              }
                          },
                          [&component, &instance, &toRemove](const UpdateActionRemove&) {
                              qCDebug(instanceProfileResolveC)
                                  << instance->name() << "|"
                                  << "UpdateActionRemove" << component->getID() << ":" << component->getVersion() << "removing";
                              toRemove.append(component->getID());
                          },
                          [this, &component, &instance, &addedActions, &componentIndex](const UpdateActionImportantChanged& ic) {
                              qCDebug(instanceProfileResolveC)
                                  << instance->name() << "|"
                                  << "UpdateImportantChanged" << component->getID() << ":" << component->getVersion() << "was changed from"
                                  << ic.oldVersion << "updating linked components";
                              auto oldVersion = APPLICATION->metadataIndex()->getLoadedVersion(component->getID(), ic.oldVersion);
                              for (auto oldReq : oldVersion->requiredSet()) {
                                  auto currentlyRequired = component->m_cachedRequires.find(oldReq);
                                  if (currentlyRequired == component->m_cachedRequires.cend()) {
                                      auto oldReqComp = componentIndex.find(oldReq.uid);
                                      if (oldReqComp != componentIndex.cend()) {
                                          (*oldReqComp)->setUpdateAction(UpdateAction{ UpdateActionRemove{} });
                                          addedActions = true;
                                      }
                                  }
                              }
                              auto linked = collectTreeLinked(component->getID());
                              for (auto comp : linked) {
                                  if (comp->isCustom()) {
                                      continue;
                                  }
                                  auto compUid = comp->getID();
                                  auto parentReq = std::find_if(component->m_cachedRequires.begin(), component->m_cachedRequires.end(),
                                                                [compUid](const Meta::Require& req) { return req.uid == compUid; });
                                  if (parentReq != component->m_cachedRequires.end()) {
                                      auto newVersion = parentReq->equalsVersion.isEmpty() ? parentReq->suggests : parentReq->equalsVersion;
                                      if (!newVersion.isEmpty()) {
                                          comp->setUpdateAction(UpdateAction{ UpdateActionChangeVersion{ newVersion } });
                                      } else {
                                          comp->setUpdateAction(UpdateAction{ UpdateActionLatestRecommendedCompatible{
                                              component->getID(),
                                              component->getName(),
                                              component->getVersion(),
                                          } });
                                      }
                                  } else {
                                      comp->setUpdateAction(UpdateAction{ UpdateActionLatestRecommendedCompatible{
                                          component->getID(),
                                          component->getName(),
                                          component->getVersion(),
                                      } });
                                  }
                                  addedActions = true;
                              }
                          } };
            std::visit(visitor, action);
            component->clearUpdateAction();
            for (auto uid : toRemove) {
                d->m_profile->remove(uid);
            }
        }
    } while (addedActions);
}

void ComponentUpdateTask::finalizeComponents()
{
    auto& components = d->m_profile->d->components;
    auto& componentIndex = d->m_profile->d->componentIndex;
    for (auto component : components) {
        for (auto req : component->m_cachedRequires) {
            auto found = componentIndex.find(req.uid);
            if (found == componentIndex.cend()) {
                component->addComponentProblem(
                    ProblemSeverity::Error,
                    QObject::tr("%1 is missing requirement %2 %3")
                        .arg(component->getName(), req.uid, req.equalsVersion.isEmpty() ? req.suggests : req.equalsVersion));
            } else {
                auto reqComp = *found;
                if (!reqComp->getProblems().isEmpty()) {
                    component->addComponentProblem(
                        reqComp->getProblemSeverity(),
                        QObject::tr("%1, a dependency of this component, has reported issues").arg(reqComp->getName()));
                }
                if (!req.equalsVersion.isEmpty() && req.equalsVersion != reqComp->getVersion()) {
                    component->addComponentProblem(ProblemSeverity::Error,
                                                   QObject::tr("%1, a dependency of this component, is not the required version %2")
                                                       .arg(reqComp->getName(), req.equalsVersion));
                } else if (!req.suggests.isEmpty() && req.suggests != reqComp->getVersion()) {
                    component->addComponentProblem(ProblemSeverity::Warning,
                                                   QObject::tr("%1, a dependency of this component, is not the suggested version %2")
                                                       .arg(reqComp->getName(), req.suggests));
                }
            }
        }
        for (auto conflict : component->knownConflictingComponents()) {
            auto found = componentIndex.find(conflict);
            if (found != componentIndex.cend()) {
                auto foundComp = *found;
                if (foundComp->isCustom()) {
                    continue;
                }
                component->addComponentProblem(
                    ProblemSeverity::Warning,
                    QObject::tr("%1 and %2 are known to not work together. It is recommended to remove one of them.")
                        .arg(component->getName(), foundComp->getName()));
            }
        }
    }
}

void ComponentUpdateTask::remoteLoadSucceeded(size_t taskIndex)
{
    if (static_cast<size_t>(d->remoteLoadStatusList.size()) < taskIndex) {
        qCWarning(instanceProfileResolveC) << "Got task index outside of results" << taskIndex;
        return;
    }
    auto& taskSlot = d->remoteLoadStatusList[taskIndex];
    disconnect(taskSlot.task.get(), &Task::succeeded, this, nullptr);
    disconnect(taskSlot.task.get(), &Task::failed, this, nullptr);
    disconnect(taskSlot.task.get(), &Task::aborted, this, nullptr);
    if (taskSlot.finished) {
        qCWarning(instanceProfileResolveC) << "Got multiple results from remote load task" << taskIndex;
        return;
    }
    qCDebug(instanceProfileResolveC) << "Remote task" << taskIndex << "succeeded";
    taskSlot.succeeded = false;
    taskSlot.finished = true;
    d->remoteTasksInProgress--;
    // update the cached data of the component from the downloaded version file.
    if (taskSlot.type == RemoteLoadStatus::Type::Version) {
        auto component = d->m_profile->getComponent(taskSlot.PackProfileIndex);
        component->m_loaded = true;
        component->updateCachedData();
    }
    checkIfAllFinished();
}

void ComponentUpdateTask::remoteLoadFailed(size_t taskIndex, const QString& msg)
{
    if (static_cast<size_t>(d->remoteLoadStatusList.size()) < taskIndex) {
        qCWarning(instanceProfileResolveC) << "Got task index outside of results" << taskIndex;
        return;
    }
    auto& taskSlot = d->remoteLoadStatusList[taskIndex];
    disconnect(taskSlot.task.get(), &Task::succeeded, this, nullptr);
    disconnect(taskSlot.task.get(), &Task::failed, this, nullptr);
    disconnect(taskSlot.task.get(), &Task::aborted, this, nullptr);
    if (taskSlot.finished) {
        qCWarning(instanceProfileResolveC) << "Got multiple results from remote load task" << taskIndex;
        return;
    }
    qCDebug(instanceProfileResolveC) << "Remote task" << taskIndex << "failed:" << msg;
    d->remoteLoadSuccessful = false;
    taskSlot.succeeded = false;
    taskSlot.finished = true;
    d->remoteTasksInProgress--;
    checkIfAllFinished();
}

void ComponentUpdateTask::checkIfAllFinished()
{
    setProgress(m_progress + 1, m_progressTotal);
    if (d->remoteTasksInProgress) {
        // not yet...
        return;
    }
    if (d->remoteLoadSuccessful) {
        // nothing bad happened... clear the temp load status and proceed with looking at dependencies
        d->remoteLoadStatusList.clear();
        performUpdateActions();
        resolveDependencies(d->mode == Mode::Launch);
    } else {
        // remote load failed... report error and bail
        QStringList allErrorsList;
        for (auto& item : d->remoteLoadStatusList) {
            if (!item.succeeded) {
                const ComponentPtr component = d->m_profile->getComponent(item.PackProfileIndex);
                allErrorsList.append(tr("Could not download metadata for %1 %2. Please change the version or try again later.")
                                         .arg(component->getName(), component->m_version));
            }
        }
        d->remoteLoadStatusList.clear();

        auto allErrors = allErrorsList.join("\n");
        emitFailed(tr("Component metadata update task failed while downloading from remote server:\n%1").arg(allErrors));
    }
}
