/*
 * Copyright (C) by Klaas Freitag <freitag@owncloud.com>
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program 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 General Public License
 * for more details.
 */

#include "folderman.h"

#include "account.h"
#include "accountmanager.h"
#include "accountstate.h"
#include "accessmanager.h"
#include "common/asserts.h"
#include "configfile.h"
#include "folder.h"
#include "gui/networkinformation.h"
#include "guiutility.h"
#include "libsync/syncengine.h"
#include "lockwatcher.h"
#include "scheduling/syncscheduler.h"
#include "socketapi/socketapi.h"
#include "spacesmanager.h"
#include "syncresult.h"
#include "theme.h"


#ifdef Q_OS_WIN
#include "common/utility_win.h"
#endif

#include <QMessageBox>
#include <QMutableSetIterator>
#include <QNetworkProxy>
#include <QStringLiteral>

using namespace std::chrono;
using namespace std::chrono_literals;

namespace {
qsizetype numberOfSyncJournals(const QString &path)
{
    return QDir(path).entryList({QStringLiteral(".sync_*.db"), QStringLiteral("._sync_*.db")}, QDir::Hidden | QDir::Files).size();
}
}

namespace OCC {
Q_LOGGING_CATEGORY(lcFolderMan, "gui.folder.manager", QtInfoMsg)

void TrayOverallStatusResult::addResult(Folder *f)
{
    _overallStatus._numNewConflictItems += f->syncResult()._numNewConflictItems;
    _overallStatus._numErrorItems += f->syncResult()._numErrorItems;
    _overallStatus._numBlacklistErrors += f->syncResult()._numBlacklistErrors;

    auto time = f->lastSyncTime();
    if (time > lastSyncDone) {
        lastSyncDone = time;
    }

    // use status of the folder
    auto status = f->syncPaused() || NetworkInformation::instance()->isBehindCaptivePortal() || NetworkInformation::instance()->isMetered()
        ? SyncResult::Paused
        : f->syncResult().status();
    // in case the linked account is not connected -> we are offline
    if (!f->isConnected()) {
        status = SyncResult::Offline;
    }
    // undefined state means we are in trouble
    if (status == SyncResult::Undefined) {
        status = SyncResult::Problem;
    }
    if (status > _overallStatus.status()) {
        _overallStatus.setStatus(status);
    }
}

const SyncResult &TrayOverallStatusResult::overallStatus() const
{
    return _overallStatus;
}

FolderMan *FolderMan::_instance = nullptr;

FolderMan::FolderMan()
    : _lockWatcher(new LockWatcher)
    , _scheduler(new SyncScheduler(this))
    , _socketApi(new SocketApi)
{
    auto settings = ConfigFile::makeQSettings();
    if (settings.contains(IgnoreHiddenFilesKey)) {
        _ignoreHiddenFiles = settings.value(IgnoreHiddenFilesKey).toBool();
    } else {
        settings.setValue(IgnoreHiddenFilesKey, _ignoreHiddenFiles); // defaults to true
    }

    connect(AccountManager::instance(), &AccountManager::accountRemoved, this, &FolderMan::slotRemoveFoldersForAccount);

    connect(_lockWatcher.data(), &LockWatcher::fileUnlocked, this, [this](const QString &path, FileSystem::LockMode) {
        if (Folder *f = folderForPath(path)) {
            // Treat this equivalently to the file being reported by the file watcher
            f->slotWatchedPathsChanged({path}, Folder::ChangeReason::UnLock);
        }
    });
}

FolderMan *FolderMan::instance()
{
    Q_ASSERT(_instance);
    return _instance;
}

FolderMan::~FolderMan()
{
    unloadAndDeleteAllFolders();
    _instance = nullptr;
}

QList<Folder *> FolderMan::folders() const
{
    QList<Folder *> allFolders;
    QList accountIds = _folders.keys();
    for (QUuid id : std::as_const(accountIds))
        allFolders.append(_folders[id].values());

    return allFolders;
}

QList<Folder *> FolderMan::foldersForAccount(const QUuid &accountId)
{
    if (_folders.contains(accountId))
        return _folders[accountId].values();
    return {};
}

void FolderMan::unloadAndDeleteAllFolders()
{
    if (_folders.isEmpty())
        return;

    // notify interested parties *before* the folders are actually deleted - this is important in eg etagwatcher and the activity
    // tabs because they need the original folder pointers to update themselves. todo: review the use of the folder pointers in these
    // classes
    emit folderListChanged(QUuid(), {});

    // save all folder definitions using a single settings instance to avoid file write/read churn
    // the settings will be synced to file when the instance goes out of scope, at the latest
    QSettings settings = ConfigFile::makeQSettings();
    settings.beginGroup("Accounts");
    // clear the list of existing folders.
    QList<QUuid> accountIds = _folders.keys();
    for (auto id : std::as_const(accountIds)) {
        for (Folder *folder : std::as_const(_folders[id])) {
            saveFolder(folder, settings);
            _socketApi->slotUnregisterPath(folder);
            folder->deleteLater();
        }
        _folders[id].clear();
        emit unsyncedSpaceCountChanged(id, 0, 0);
    }
    _folders.clear();
    _unsyncedSpaces.clear();
    Q_ASSERT(_folders.isEmpty());
}

void FolderMan::registerFolderWithSocketApi(Folder *folder)
{
    if (!folder)
        return;
    if (!QDir(folder->path()).exists())
        return;

    // register the folder with the socket API
    if (folder->canSync())
        _socketApi->slotRegisterPath(folder);
}

std::optional<qsizetype> FolderMan::setupFoldersFromConfig()
{
    setSyncEnabled(false);

    // todo: #9
    unloadAndDeleteAllFolders();

    auto settings = ConfigFile::makeQSettings();
    settings.beginGroup("Accounts");

    const auto &accountsWithSettings = settings.childGroups();
    qCInfo(lcFolderMan) << "Setup folders from settings file";

    for (const auto &account : AccountManager::instance()->accounts()) {
        const auto accountIndex = account->account()->groupIndex();
        Q_ASSERT(!accountIndex.isEmpty());
        if (!accountsWithSettings.contains(accountIndex)) {
            qCWarning(lcFolderMan) << "Account id from account manager is missing from Config";
            continue;
        }

        // Process config for this account.
        if (!addFoldersFromConfigByAccount(settings, account)) {
            return {};
        }
    }
    settings.endGroup(); // "Accounts"

    if (!_folders.empty()) {
        // couldn't this be "schedule new folders"?
        scheduleAllFolders();
        setSyncEnabled(true);
    }

    return _folders.size();
}

bool FolderMan::addFoldersFromConfigByAccount(QSettings &settings, AccountState *account)
{
    if (!account || !account->account() || !account->account()->spacesManager())
        return false;

    settings.beginGroup(QStringLiteral("%1/Folders").arg(account->account()->groupIndex()));

    const auto &childGroups = settings.childGroups();
    for (const auto &folderAlias : childGroups) {
        settings.beginGroup(folderAlias);

        FolderDefinition folderDefinition = FolderDefinition::load(settings, folderAlias.toUtf8());
        // this should NEVER happen
        Q_ASSERT(!folderDefinition.id().isEmpty());

        // this can only happen when loading from config
        // does not belong in general addFolder routine
        if (SyncJournalDb::dbIsTooNewForClient(folderDefinition.absoluteJournalPath())) {
            return false;
        }

        Folder *folder = addFolder(account, folderDefinition);
        if (!folder) {
            continue;
        }

        // this is checked before the spaces manager has loaded the spaces so it will always be null.
        // this is a bit odd for now as in the gui it shows the unavailable state until after login. I think
        // in most cases this will be ok as it's not a common workflow, we just have it on demo
        // if we don't set this here a sync will be attempted, then it just shows as failed.
        folder->setAvailable(folder->space() != nullptr);

        settings.endGroup(); // folderAlias
    }
    settings.endGroup(); // accountId\Folders

    GraphApi::SpacesManager *spaceMan = account->account()->spacesManager();
    _scheduler->connectSpacesManager(spaceMan);
    connect(spaceMan, &GraphApi::SpacesManager::spacesAdded, this, &FolderMan::onSpacesAdded);
    connect(spaceMan, &GraphApi::SpacesManager::spacesRemoved, this, &FolderMan::onSpacesRemoved);

    QUuid accountId = account->account()->uuid();
    emit folderListChanged(accountId, _folders[accountId].values());
    // at this stage the spaces manager tends to be empty
    int spaceCount = spaceMan->spacesCount();
    int unsyncedCount = _unsyncedSpaces.contains(accountId) ? _unsyncedSpaces[accountId].count() : 0;
    emit unsyncedSpaceCountChanged(accountId, unsyncedCount, spaceCount);

    return true;
}

void FolderMan::setUpInitialSyncFolders(AccountState *accountState, bool useVfs)
{
    if (accountState && accountState->account() && accountState->account()->spacesManager()) {
        GraphApi::SpacesManager *spaceMan = accountState->account()->spacesManager();

        // this replaces the old use of SpacesManager::checkReady which was overcomplicated.
        // short explanation: the spaces manager has to wait for the account credentials to be valid before it can load its spaces
        // folderman can't "force" that condiiton so it just has to wait for that setup to complete
        // normally the spaces manager should always be ready at this point, but if for whatever reason it's not, catch it when it is
        if (!spaceMan->isReady())
            connect(spaceMan, &GraphApi::SpacesManager::ready, this, [this, accountState, useVfs] { loadSpacesAndCreateFolders(accountState, useVfs); });
        else
            loadSpacesAndCreateFolders(accountState, useVfs);
    }
}

void FolderMan::loadSpacesAndCreateFolders(AccountState *accountState, bool useVfs)
{
    if (!accountState || !accountState->account())
        return;

    GraphApi::SpacesManager *spacesMgr = accountState->account()->spacesManager();
    if (!spacesMgr)
        return;

    auto spaces = spacesMgr->spaces();

    if (!spaces.isEmpty()) {
        setSyncEnabled(false);

        QSettings settings = ConfigFile::makeQSettings();
        settings.beginGroup("Accounts");

        // prepare the root - reality check this as I think the user can change this from default? Yes they can but not for auto-loaded
        // folders eg on account creation, which is what is happening here.
        const QString localDir(accountState->account()->defaultSyncRoot());
        if (!prepareFolder(localDir)) {
            return;
        }

        Utility::setupFavLink(localDir);

        for (auto *space : std::as_const(spaces)) {
            FolderDefinition folderDef(space->webDavUrl(), space->id(), space->displayName());

            folderDef.setPriority(space->sortPriority());

            QString localPath = findGoodPathForNewSyncFolder(localDir, folderDef.displayName(), NewFolderType::SpacesFolder, space->accountId());
            folderDef.setLocalPath(localPath);
            folderDef.setTargetPath({});

            Folder *folder = addFolderFromScratch(accountState, std::move(folderDef), useVfs);
            if (folder) {
                saveFolder(folder, settings);
                // oof - todo #50
                _scheduler->enqueueFolder(folder, SyncScheduler::Priority::High);
            }
        }
        setSyncEnabled(true);
        _scheduler->connectSpacesManager(spacesMgr);
        // initial load is complete, now we just wait for spaces to be added or removed incrementally
        connect(spacesMgr, &GraphApi::SpacesManager::spacesAdded, this, &FolderMan::onSpacesAdded);
        connect(spacesMgr, &GraphApi::SpacesManager::spacesRemoved, this, &FolderMan::onSpacesRemoved);

        QUuid id = accountState->account()->uuid();
        emit folderListChanged(id, _folders[id].values());
        int spaceCount = spacesMgr->spacesCount();
        emit unsyncedSpaceCountChanged(id, _unsyncedSpaces[id].count(), spaceCount);
    }
}

void FolderMan::setUpInitialSpaces(AccountState *accountState)
{
    if (accountState && accountState->account() && accountState->account()->spacesManager()) {
        GraphApi::SpacesManager *spaceMan = accountState->account()->spacesManager();

        // this replaces the old use of SpacesManager::checkReady which was overcomplicated.
        // short explanation: the spaces manager has to wait for the account credentials to be valid before it can load its spaces
        // folderman can't "force" that condiiton so it just has to wait for that setup to complete
        // normally the spaces manager should always be ready at this point, but if for whatever reason it's not, catch it when it is
        if (!spaceMan->isReady())
            connect(spaceMan, &GraphApi::SpacesManager::ready, this, [this, accountState] { loadSpacesAlone(accountState); });
        else
            loadSpacesAlone(accountState);
    }
}

void FolderMan::loadSpacesAlone(AccountState *accountState)
{
    if (!accountState || !accountState->account())
        return;

    GraphApi::SpacesManager *spacesMgr = accountState->account()->spacesManager();
    if (!spacesMgr)
        return;

    auto spaces = spacesMgr->spaces();

    _scheduler->connectSpacesManager(spacesMgr);

    QUuid id = accountState->account()->uuid();
    int spaceCount = spacesMgr->spacesCount();
    onSpacesAdded(id, spaces, spaceCount);

    // initial load is complete, now we just wait for spaces to be added or removed incrementally
    connect(spacesMgr, &GraphApi::SpacesManager::spacesAdded, this, &FolderMan::onSpacesAdded);
    connect(spacesMgr, &GraphApi::SpacesManager::spacesRemoved, this, &FolderMan::onSpacesRemoved);
}


void FolderMan::onSpacesAdded(const QUuid &accountId, QList<GraphApi::Space *> spaces, int totalSpaceCount)
{
    // not a fan but the spaces manager doesn't have the account state, just the account. At the point that we correct the relationship
    // so that account "owns" account state, the spacesManager can include the account pointer in the signal to eliminate
    // this "need" for the singleton
    AccountState *accountState = AccountManager::instance()->accountState(accountId);
    if (!accountState || !accountState->account())
        return;

    QSet<GraphApi::Space *> newUnsyncedSpaces(spaces.cbegin(), spaces.cend());

    if (!_unsyncedSpaces.contains(accountId)) {
        _unsyncedSpaces.insert(accountId, {});
    }

    for (auto space : std::as_const(spaces)) {
        // be sure to eliminate any spaces that are already in the unsynced list to prevent auto-adding spaces that the
        // user explicitly removed from sync
        if (_unsyncedSpaces.contains(accountId) && _unsyncedSpaces[accountId].contains(space->id()))
            newUnsyncedSpaces.remove(space);
        else if (_folders.contains(accountId) && _folders[accountId].contains(space->id())) {
            newUnsyncedSpaces.remove(space);
            _folders[accountId][space->id()]->setAvailable(true);
        }
    }

    if (newUnsyncedSpaces.isEmpty()) {
        // this normally happens when the folderman has loaded folders from config - the folders are created
        // before the spaces manager is ready so space count at that stage is zero, we catch the change here instead
        emit unsyncedSpaceCountChanged(accountId, _unsyncedSpaces[accountId].count(), totalSpaceCount);
        return;
    }

    if (Theme::instance()->syncNewlyDiscoveredSpaces()) {
        // Refactoring todo: why was this scheduled for "later" on the main event loop? aren't we already there?
        // where does this slot run if not on the main thread?
        // what needs to be processed "before" this loading routine that requires scheduling it for later?
        // if anything we should consider running the loading routines in a worker thread to avoid *blocking* the main
        // event loop.
        //  QTimer::singleShot(0, this, [this, account, unsycnedSpaces]() {
        for (GraphApi::Space *newSpace : newUnsyncedSpaces) {
            qCInfo(lcFolderMan) << "Adding sync connection for newly discovered space" << newSpace->displayName();

            const QString localDir(accountState->account()->defaultSyncRoot());
            const QString folderName = findGoodPathForNewSyncFolder(localDir, newSpace->displayName(), NewFolderType::SpacesFolder, accountId);

            // I'm recycling the routine used in the folder wizard to add a new space from gui for now. No I don't
            // like it but I am out of steam trying to collate all these barely different methods of creating a folder
            // into something sensible right now - I will come back to it
            SyncConnectionDescription fwr;
            fwr.davUrl = newSpace->webDavUrl();
            fwr.spaceId = newSpace->id();
            fwr.localPath = folderName;
            fwr.displayName = newSpace->displayName();
            fwr.useVirtualFiles = Utility::isWindows() ? Theme::instance()->showVirtualFilesOption() : false;
            fwr.sortPriority = newSpace->sortPriority();

            addFolderFromGui(accountState, fwr);
        }
        newUnsyncedSpaces.clear();
    } else {
        for (auto *sp : std::as_const(newUnsyncedSpaces))
            _unsyncedSpaces[accountId].insert(sp->id(), sp);
    }

    emit unsyncedSpaceCountChanged(accountId, _unsyncedSpaces[accountId].count(), totalSpaceCount);
}

void FolderMan::onSpacesRemoved(const QUuid &accountId, QList<QString> spaceIds, int totalSpaceCount)
{
    if (!_unsyncedSpaces.contains(accountId) && !_folders.contains(accountId))
        return;

    for (const QString &id : std::as_const(spaceIds)) {
        _unsyncedSpaces[accountId].remove(id);

        if (_folders[accountId].contains(id)) {
            Folder *f = _folders[accountId][id];
            f->setAvailable(false);
        }
    }

    emit unsyncedSpaceCountChanged(accountId, _unsyncedSpaces[accountId].count(), totalSpaceCount);
}


bool FolderMan::ensureJournalGone(const QString &journalDbFile)
{
    // remove the old journal file
    while (QFile::exists(journalDbFile) && !QFile::remove(journalDbFile)) {
        qCWarning(lcFolderMan) << "Could not remove old db file at" << journalDbFile;
        int ret = QMessageBox::warning(nullptr, tr("Could not reset folder state"),
            tr("An old sync journal '%1' was found, "
               "but could not be removed. Please make sure "
               "that no application is currently using it.")
                .arg(QDir::fromNativeSeparators(QDir::cleanPath(journalDbFile))),
            QMessageBox::Retry | QMessageBox::Abort);
        if (ret == QMessageBox::Abort) {
            return false;
        }
    }
    return true;
}

bool FolderMan::ensureFilesystemSupported(const FolderDefinition &folderDefinition)
{
    if (Utility::isMac()) {
        QString filesystemType = FileSystem::fileSystemForPath(folderDefinition.localPath());
        if (filesystemType != QStringLiteral("apfs")) {
            QMessageBox::warning(nullptr, tr("Unsupported filesystem"), tr("On macOS, only the Apple File System is supported."), QMessageBox::Ok);

            return false;
        }
    }

    return true;
}

SocketApi *FolderMan::socketApi()
{
    return _socketApi.get();
}

void FolderMan::slotFolderSyncPauseChanged(Folder *f, bool paused)
{
    if (!f) {
        qCCritical(lcFolderMan) << "slotFolderSyncPaused called with empty folder";
        return;
    }

    if (!paused) {
        _disabledFolders.remove(f);
        if (f->canSync()) {
            scheduler()->enqueueFolder(f);
        }
    } else {
        _disabledFolders.insert(f);
    }
}

void FolderMan::slotFolderCanSyncChanged()
{
    Folder *f = qobject_cast<Folder *>(sender());
    OC_ASSERT(f);
    if (f->canSync()) {
        _socketApi->slotRegisterPath(f);
    } else {
        _socketApi->slotUnregisterPath(f);
    }
}

Folder *FolderMan::folder(const QUuid &accountId, const QString &spaceId) const
{
    if (_folders.contains(accountId) && _folders[accountId].contains(spaceId))
        return _folders[accountId][spaceId];

    return nullptr;
}

// maybe add a new arg for prio level
void FolderMan::scheduleFoldersForAccount(const QUuid &accountId)
{
    if (!_folders.contains(accountId))
        return;

    for (Folder *f : std::as_const(_folders[accountId])) {
        if (f && f->canSync())
            _scheduler->enqueueFolder(f);
    }
}

void FolderMan::scheduleAllFolders()
{
    QList<QUuid> accountIds = _folders.keys();
    for (const QUuid &id : std::as_const(accountIds))
        scheduleFoldersForAccount(id);
}

void FolderMan::slotSyncOnceFileUnlocks(const QString &path, FileSystem::LockMode mode)
{
    _lockWatcher->addFile(path, mode);
}

void FolderMan::slotIsConnectedChanged()
{
    AccountState *accountState(qobject_cast<AccountState *>(sender()));

    if (!accountState || !accountState->account())
        return;

    QString accountName = accountState->account()->displayNameWithHost();
    QUuid accountId = accountState->account()->uuid();

    if (accountState->isConnected()) {
        qCInfo(lcFolderMan) << "Account" << accountName << "connected, scheduling its folders";
        scheduleFoldersForAccount(accountId);
    } else {
        qCInfo(lcFolderMan) << "Account" << accountName
                            << "disconnected or paused, "
                               "terminating or descheduling sync folders";

        for (auto *f : std::as_const(_folders[accountId])) {
            if (f && f->isSyncRunning()) {
                f->slotTerminateSync(tr("Account disconnected or paused"));
            }
        }
    }
}

// only enable or disable foldermans will schedule and do syncs.
// this is not the same as Pause and Resume of folders.
void FolderMan::setSyncEnabled(bool enabled)
{
    if (enabled) {
        // We have things in our queue that were waiting for the connection to come back on.
        scheduler()->start();
    } else {
        scheduler()->stop();
    }
    qCInfo(lcFolderMan) << Q_FUNC_INFO << enabled;
    // force a redraw in case the network connect status changed
    Q_EMIT folderSyncStateChange(nullptr);
}

void FolderMan::slotRemoveFoldersForAccount(AccountState *accountState)
{
    if (!accountState || !accountState->account() || !_folders.contains(accountState->account()->uuid())) {
        return;
    }

    QSettings settings = ConfigFile::makeQSettings();
    QString accountGroup = QStringLiteral("Accounts/%1").arg(accountState->account()->groupIndex());
    settings.beginGroup(accountGroup);

    QUuid id = accountState->account()->uuid();
    // we always should emit before the folders have actually been deleted. consider renaming
    emit folderListChanged(id, {});

    for (const auto &f : std::as_const(_folders[id])) {
        removeFolderSettings(f, settings);
        deleteFolderSync(f);
    }

    _folders.remove(id);
    _unsyncedSpaces.remove(id);
    // this is probably pointless but should not hurt
    emit unsyncedSpaceCountChanged(id, 0, 0);
}

void FolderMan::removeFolderSettings(Folder *folder, QSettings &settings)
{
    if (!folder) {
        return;
    }
    disconnectAutoSave(folder);
    QString id = QString::fromUtf8(folder->id());
    if (id.isEmpty())
        return;
    settings.remove(QStringLiteral("Folders/%1").arg(id));
    settings.remove(QStringLiteral("Multifolders/%1").arg(id));
    settings.remove(QStringLiteral("FoldersWithPlaceholders/%1").arg(id));
}

void FolderMan::removeFolderSettings(Folder *folder)
{
    QSettings settings = ConfigFile::makeQSettings();
    QString accountGroup = QStringLiteral("Accounts/%1").arg(folder->accountState()->account()->groupIndex());
    settings.beginGroup(accountGroup);
    removeFolderSettings(folder, settings);
}

void FolderMan::slotServerVersionChanged(Account *account)
{
    if (!account || !_folders.contains(account->uuid()))
        return;

    // Pause folders if the server version is unsupported
    if (account->serverSupportLevel() == Account::ServerSupportLevel::Unsupported) {
        qCWarning(lcFolderMan) << "The server version is unsupported:" << account->capabilities().status().versionString()
                               << "pausing all folders on the account";

        for (auto &f : _folders[account->uuid()]) {
            f->setSyncPaused(true);
        }
    }
}

void FolderMan::slotFolderSyncStarted()
{
    auto f = qobject_cast<Folder *>(sender());
    OC_ASSERT(f);
    if (!f)
        return;

    qCInfo(lcFolderMan) << ">========== Sync started for folder [" << f->shortGuiLocalPath() << "] of account ["
                        << f->accountState()->account()->displayNameWithHost() << "] with remote [" << f->remoteUrl().toDisplayString() << "]";
}

/*
 * a folder indicates that its syncing is finished.
 * Start the next sync after the system had some milliseconds to breath.
 * This delay is particularly useful to avoid late file change notifications
 * (that we caused ourselves by syncing) from triggering another spurious sync.
 */
void FolderMan::slotFolderSyncFinished(const SyncResult &)
{
    auto f = qobject_cast<Folder *>(sender());
    OC_ASSERT(f);
    if (!f)
        return;

    qCInfo(lcFolderMan) << "<========== Sync finished for folder [" << f->shortGuiLocalPath() << "] of account ["
                        << f->accountState()->account()->displayNameWithHost() << "] with remote [" << f->remoteUrl().toDisplayString() << "]";
}

bool FolderMan::validateFolderDefinition(const FolderDefinition &folderDefinition)
{
    if (folderDefinition.id().isEmpty() || folderDefinition.journalPath().isEmpty() || !ensureFilesystemSupported(folderDefinition))
        return false;
    return true;
}


Folder *FolderMan::addFolder(AccountState *accountState, const FolderDefinition &folderDefinition)
{
    if (!accountState || !accountState->account())
        return nullptr;

    if (!validateFolderDefinition(folderDefinition)) {
        qCWarning(lcFolderMan) << "Folder Definition validation failed for folder" << folderDefinition.localPath();
        return nullptr;
    }

    QUuid accountId = accountState->account()->uuid();

    if (Folder *f = folder(accountId, folderDefinition.spaceId())) {
        qCWarning(lcFolderMan) << "Trying to add folder" << folderDefinition.localPath() << "but it already exists in folder list";
        return f;
    }

    auto vfs = VfsPluginManager::instance().createVfsFromPlugin(folderDefinition.virtualFilesMode());
    if (!vfs) {
        qCWarning(lcFolderMan) << "Could not load plugin for mode" << folderDefinition.virtualFilesMode();
        return nullptr;
    }

    auto folder = new Folder(folderDefinition, accountState, std::move(vfs), _ignoreHiddenFiles, this);

    qCInfo(lcFolderMan) << "Adding folder to Folder Map " << folder << folder->path();
    QString spaceId = folder->spaceId();
    // always add the folder even if it had a setup error - future add special handling for incomplete folders if possible
    _folders[accountId].insert(spaceId, folder);

    if (folder->syncPaused()) {
        _disabledFolders.insert(folder);
    }

    if (!folder->hasSetupError()) {
        connectFolder(folder);
        Q_EMIT folderSyncStateChange(folder);
    }

    return folder;
}

void FolderMan::connectFolder(Folder *folder)
{
    // See matching disconnects in disconnectFolder().
    if (folder && !folder->hasSetupError()) {
        connect(folder, &Folder::syncStateChange, _socketApi.get(), [folder, this] { _socketApi->slotUpdateFolderView(folder); });
        connect(folder, &Folder::syncStarted, this, &FolderMan::slotFolderSyncStarted);
        connect(folder, &Folder::syncFinished, this, &FolderMan::slotFolderSyncFinished);
        connect(folder, &Folder::syncStateChange, this, [folder, this] { Q_EMIT folderSyncStateChange(folder); });
        connect(folder, &Folder::syncPausedChanged, this, &FolderMan::slotFolderSyncPauseChanged);
        connect(folder, &Folder::canSyncChanged, this, &FolderMan::slotFolderCanSyncChanged);
        // format wants to move the pointer "*" one space away from the type which = clazy not normalized sig warning
        // clang-format off
        connect(folder, SIGNAL(syncPausedChanged(Folder*,bool)), this, SLOT(saveFolder(Folder*)));
        connect(folder, SIGNAL(vfsModeChanged(Folder*,Vfs::Mode)), this, SLOT(saveFolder(Folder*)));
        // clang-format on  
        connect(
            &folder->syncEngine().syncFileStatusTracker(), &SyncFileStatusTracker::fileStatusChanged, _socketApi.get(), &SocketApi::broadcastStatusPushMessage);
        connect(folder, &Folder::watchedFileChangedExternally, &folder->syncEngine().syncFileStatusTracker(), &SyncFileStatusTracker::slotPathTouched);

        registerFolderWithSocketApi(folder);
    }
}

void FolderMan::disconnectAutoSave(Folder *folder)
{
    // clang-format off
    disconnect(folder, SIGNAL(syncPausedChanged(Folder*,bool)), this, SLOT(saveFolder(Folder*)));
    disconnect(folder, SIGNAL(vfsModeChanged(Folder*,Vfs::Mode)), this, SLOT(saveFolder(Folder*)));
    // clang-format on  
}

void FolderMan::disconnectFolder(Folder *folder)
{
    if (folder && !folder->hasSetupError()) {
        _socketApi->slotUnregisterPath(folder);

        disconnect(folder, nullptr, _socketApi.get(), nullptr);
        disconnect(folder, nullptr, this, nullptr);
        disconnect(&folder->syncEngine(), nullptr, folder, nullptr);
        disconnect(
            &folder->syncEngine().syncFileStatusTracker(), &SyncFileStatusTracker::fileStatusChanged, _socketApi.get(), &SocketApi::broadcastStatusPushMessage);
        disconnect(folder, nullptr, &folder->syncEngine().syncFileStatusTracker(), nullptr);
    }
}

void FolderMan::saveFolder(Folder *folder, QSettings &settings)
{
    Q_ASSERT(settings.group() == QStringLiteral("Accounts"));

    auto strId = QString::fromUtf8(folder->id());
    QString targetGroup = QStringLiteral("%1/Folders/%2").arg(folder->accountState()->account()->groupIndex(), strId);
    settings.beginGroup(targetGroup);
    FolderDefinition::save(settings, folder->definition());
    settings.endGroup();
}

void FolderMan::saveFolder(Folder *folder)
{
    QSettings settings = ConfigFile::makeQSettings();
    settings.beginGroup("Accounts");
    saveFolder(folder, settings);
}

Folder *FolderMan::folderForPath(const QString &path, QString *relativePath) const
{
    QString absolutePath = QDir::cleanPath(path) + QLatin1Char('/');

    for (Folder *folder : folders()) {
        const QString folderPath = folder->cleanPath() + QLatin1Char('/');

        if (absolutePath.startsWith(folderPath, (Utility::isWindows() || Utility::isMac()) ? Qt::CaseInsensitive : Qt::CaseSensitive)) {
            if (relativePath) {
                *relativePath = absolutePath.mid(folderPath.length());
                relativePath->chop(1); // we added a '/' above
            }
            return folder;
        }
    }

    if (relativePath)
        relativePath->clear();
    return nullptr;
}

void FolderMan::removeFolderFromGui(Folder *f)
{
    Q_ASSERT(f->accountState() && f->accountState()->account());

    QUuid accountId = f->accountState()->account()->uuid();
    emit folderRemoved(accountId, f);
    if (f->isAvailable())
    {
        _unsyncedSpaces[accountId].insert(f->spaceId(), f->space());
        emit unsyncedSpaceCountChanged(accountId, _unsyncedSpaces[accountId].count(), f->accountState()->account()->spacesManager()->spacesCount());
    }
    removeFolderSettings(f);
    deleteFolderSync(f);
}

void FolderMan::forceFolderSync(Folder *f)
{
    if (!f)
        return;
    // Prevent new sync starts
    _scheduler->stop();

    // Terminate and reschedule any running sync
    // todo #50: evaluate whether it really makes sense to cancel folders that are ALREADY in sync - this makes no sense to me.
    // also should this be responsibility of the sync scheduler? It knows which folder is currently syncing so it could
    // cancel it directly without having to iterate ALL the folders.
    for (auto *folder : folders()) {
        if (folder->isSyncRunning()) {
            folder->slotTerminateSync(tr("User triggered force sync"));
            _scheduler->enqueueFolder(folder);
        }
    }

    f->slotWipeErrorBlacklist(); // issue #6757
    f->slotNextSyncFullLocalDiscovery(); // ensure we don't forget about local errors

    // Insert the selected folder at the front of the queue
    // this should not be a direct call, just signal a request. When that time comes move the prio enum to an independent location to avoid
    // caller deps on the scheduler
    _scheduler->enqueueFolder(f, SyncScheduler::Priority::High);

    // Restart scheduler
    _scheduler->start();
}

void FolderMan::deleteFolderSync(Folder *f)
{
    if (!f) 
        return;
    
    if (! (f->accountState() && f->accountState()->account() && _folders.contains(f->accountState()->account()->uuid())))
        return;


    qCInfo(lcFolderMan) << "Removing " << f->path();

    const bool currentlyRunning = f->isSyncRunning();
    if (currentlyRunning) {
        // abort the sync now
        f->slotTerminateSync(tr("Folder is about to be removed"));
    }

    f->setSyncPaused(true);

    // this function includes the stuff to remove the database files.
    f->wipeForRemoval();

    _folders[f->accountState()->account()->uuid()].remove(f->spaceId());

    disconnectFolder(f);

    f->deleteLater();
}

QString FolderMan::getBackupName(QString fullPathName) const
{
    if (fullPathName.endsWith(QLatin1String("/")))
        fullPathName.chop(1);

    if (fullPathName.isEmpty())
        return QString();

    QString newName = fullPathName + tr(" (backup)");
    QFileInfo fi(newName);
    int cnt = 2;
    do {
        if (fi.exists()) {
            newName = fullPathName + tr(" (backup %1)").arg(cnt++);
            fi.setFile(newName);
        }
    } while (fi.exists());

    return newName;
}

void FolderMan::setDirtyProxy()
{
    for (auto *f : folders()) {
        if (f) {
            if (f->accountState() && f->accountState()->account() && f->accountState()->account()->accessManager()) {
                // Need to do this so we do not use the old determined system proxy
                f->accountState()->account()->accessManager()->setProxy(QNetworkProxy(QNetworkProxy::DefaultProxy));
            }
        }
    }
}

// QFileInfo::canonicalPath returns an empty string if the file does not exist.
// This function also works with files that does not exist and resolve the symlinks in the
// parent directories.
static QString canonicalPath(const QString &path)
{
    QFileInfo selFile(path);
    if (!selFile.exists()) {
        const auto parentPath = selFile.dir().path();

        // It's possible for the parentPath to match the path
        // (possibly we've arrived at a non-existant drive root on Windows)
        // and recursing would be fatal.
        if (parentPath == path) {
            return path;
        }

        return canonicalPath(parentPath) + QLatin1Char('/') + selFile.fileName();
    }
    return selFile.canonicalFilePath();
}

static QString checkPathForSyncRootMarkingRecursive(const QString &path, FolderMan::NewFolderType folderType, const QUuid &accountUuid)
{
    // First do basic checks if this path is usable
    const QFileInfo selectedPathInfo(path);
    if (!selectedPathInfo.isDir()) {
        return FolderMan::tr("The selected path is not a folder.");
    }

    if (numberOfSyncJournals(selectedPathInfo.filePath()) != 0) {
        return FolderMan::tr("The folder %1 is used in a folder sync connection.").arg(QDir::toNativeSeparators(selectedPathInfo.filePath()));
    }

    // Check for sync root markings in this folder
    std::pair<QString, QUuid> existingTags = Utility::getDirectorySyncRootMarkings(path);
    if (!existingTags.first.isEmpty()) {
        if (existingTags.first != Theme::instance()->orgDomainName()) {
            // another application uses this as spaces root folder
            return FolderMan::tr("Folder '%1' is already in use by application %2.").arg(path, existingTags.first);
        }

        // Looks good, it's our app, let's check the account tag:
        switch (folderType) {
        case FolderMan::NewFolderType::SpacesFolder:
            if (existingTags.second == accountUuid) {
                // Nice, that's what we like, the sync root for our account in our app. No error, and we don't need to check the parent folders.
                return {};
            }
            [[fallthrough]];
        case FolderMan::NewFolderType::SpacesSyncRoot:
            // It's our application but we don't want to create a spaces folder, so it must be another space root
            return FolderMan::tr("Folder '%1' is already in use by another account.").arg(path);
        }
    }

    // This folder is ok, but check the parent folders if they are used

    QString parent = QFileInfo(path).path();
    if (parent == path) { // root dir, stop recursing
        return {};
    }

    return checkPathForSyncRootMarkingRecursive(parent, folderType, accountUuid);
}

/*
 * with spaces:
 *  - spaces sync root not in syncdb folder
 *  - spaces sync root not in another spaces sync root
 *
 *  - space not in syncdb folder
 *  - space *can* be in sync root
 *  - space not in spaces sync root of other account (check with account uuid)
 */
QString FolderMan::checkPathValidity(const QString &path, NewFolderType folderType, const QUuid &accountUuid) const
{
    // Do checks against existing folders. This catches cases where:
    // - the user deleted a folder from the filesystem, but there is still a folder sync connection in the configuration
    // - check if this folder contains other folders for spaces
    // If so, error out.
    const auto cs = Utility::fsCaseSensitivity();
    const QString userDir = QDir::cleanPath(canonicalPath(path)) + QLatin1Char('/');
    for (auto f : folders()) {
        const QString folderDir = QDir::cleanPath(canonicalPath(f->path())) + QLatin1Char('/');
        if (QString::compare(folderDir, userDir, cs) == 0) {
            return tr("There is already a sync from the server to this local folder.");
        }
        if (FileSystem::isChildPathOf(folderDir, userDir)) {
            return tr("The local folder %1 already contains a folder used in a folder sync connection.")
                .arg(QDir::toNativeSeparators(path));
        }

        if (FileSystem::isChildPathOf(userDir, folderDir)) {
            return tr("The local folder %1 is already contained in a folder used in a folder sync connection.")
                .arg(QDir::toNativeSeparators(path));
        }
    }

    // Now run filesystem based checks
    return findExistingFolderAndCheckValidity(path, folderType, accountUuid);
}

QString FolderMan::findExistingFolderAndCheckValidity(const QString &path, NewFolderType folderType, const QUuid &accountUuid)
{
    if (path.isEmpty()) {
        return tr("No valid folder selected.");
    }

#ifdef Q_OS_WIN
    QNtfsPermissionCheckGuard ntfs_perm;
#endif

    auto pathLengthCheck = Folder::checkPathLength(path);
    if (!pathLengthCheck) {
        return pathLengthCheck.error();
    }

    // If this is a new folder, recurse up to the first parent that exists, to see if we can use that to create a new folder
    const QFileInfo selectedPathInfo(path);
    if (!selectedPathInfo.exists()) {
        const QString parentPath = selectedPathInfo.path();
        if (parentPath != path) {
            return findExistingFolderAndCheckValidity(parentPath, folderType, accountUuid);
        }
        return tr("The selected path does not exist.");
    }

    // At this point we have an existing folder, so check if we can create files/folders here.
    // Note: we don't need to write to any parent, so we don't need to check writablility when traversing the parents.
    if (!selectedPathInfo.isWritable()) {
        return tr("You have no permission to write to the selected folder.");
    }

    // Now check if none of the parents is already used.
    return checkPathForSyncRootMarkingRecursive(canonicalPath(path), folderType, accountUuid);
}

QString FolderMan::findGoodPathForNewSyncFolder(
    const QString &basePath, const QString &newFolder, FolderMan::NewFolderType folderType, const QUuid &accountUuid) const
{
    // reserve extra characters to allow appending of a number
    const QString normalisedPath = FileSystem::createPortableFileName(basePath, FileSystem::pathEscape(newFolder), std::string_view(" (100)").size());

    // If the parent folder is a sync folder or contained in one, we can't
    // possibly find a valid sync folder inside it.
    // Example: Someone syncs their home directory. Then ~/foobar is not
    // going to be an acceptable sync folder path for any value of foobar.
    if (folderForPath(QFileInfo(normalisedPath).canonicalPath())) {
        // Any path with that parent is going to be unacceptable,
        // so just keep it as-is.
        return canonicalPath(normalisedPath);
    }
    // Count attempts and give up eventually
    {
        QString folder = normalisedPath;
        for (int attempt = 2; attempt <= 100; ++attempt) {
            if (!QFileInfo::exists(folder) && checkPathValidity(folder, folderType, accountUuid).isEmpty()) {
                return canonicalPath(folder);
            }
            folder = normalisedPath + QStringLiteral(" (%1)").arg(attempt);
        }
    }
    // we failed to find a non existing path
    return canonicalPath(normalisedPath);
}

bool FolderMan::ignoreHiddenFiles() const
{
    return _ignoreHiddenFiles;
}

void FolderMan::setIgnoreHiddenFiles(bool ignore)
{
    if (ignore == _ignoreHiddenFiles) {
        return;
    }

    setSyncEnabled(false);
    auto settings = ConfigFile::makeQSettings();
    settings.setValue(IgnoreHiddenFilesKey, ignore);
    _ignoreHiddenFiles = ignore;

    // This should be done through a signal-slot connection. However, this has to wait until the
    // engine is passed to the folder (so we can connect the signal/slot) instead of the folder
    // creating it. See the todo in the Folder constructor.
    for (Folder *folder : folders()) {
        if (folder->canSync()) {
            folder->syncEngine().setIgnoreHiddenFiles(ignore);
        }
    }

    setSyncEnabled(true);
}


bool FolderMan::isSpaceSynced(GraphApi::Space *space) const
{
    return (space && folder(space->accountId(), space->id()) != nullptr);
}

// this only seems to be triggered when changing the move to trash setting?
void FolderMan::slotReloadSyncOptions()
{
    for (auto *f : folders()) {
        if (f) {
            f->reloadSyncOptions();
        }
    }
}

Folder *FolderMan::addFolderFromScratch(AccountState *accountState, FolderDefinition &&folderDefinition, bool useVfs)
{
    if (!FolderMan::prepareFolder(folderDefinition.localPath())) {
        return nullptr;
    }

    folderDefinition.setJournalPath(SyncJournalDb::makeDbName(folderDefinition.localPath()));

    // this is here because allegedly, old clients may not remove the old journal when the sync is removed.
    // This is currently done in wipeForRemoval
    if (!ensureJournalGone(folderDefinition.absoluteJournalPath())) {
        return nullptr;
    }

    if (useVfs) {
        folderDefinition.setVirtualFilesMode(VfsPluginManager::instance().bestAvailableVfsMode());
    }

    auto newFolder = addFolder(accountState, folderDefinition);

    if (newFolder) {
        if (newFolder->vfs().mode() != Vfs::WindowsCfApi) {
            Utility::setupFavLink(folderDefinition.localPath());
        }
        qCDebug(lcFolderMan) << "Local sync folder" << folderDefinition.localPath() << "successfully created!";
    } else {
        qCWarning(lcFolderMan) << "Failed to create local sync folder!";
    }

    // we should not emit any folder list change from this function because it can be called in bulk as well as individual operations
    return newFolder;
}

// todo: #1
void FolderMan::addFolderFromGui(AccountState *accountState, const SyncConnectionDescription &description)
{
    if (!accountState || !accountState->account())
        return;

    setSyncEnabled(false);

    FolderDefinition definition(description.davUrl, description.spaceId, description.displayName);
    definition.setLocalPath(description.localPath);
    definition.setTargetPath(description.remotePath);
    definition.setPriority(description.sortPriority);
    auto f = addFolderFromScratch(accountState, std::move(definition), description.useVirtualFiles);


    if (f) {
        QUuid accountId = accountState->account()->uuid();
        saveFolder(f);
        emit folderAdded(accountId, f);

        if (_unsyncedSpaces.contains(accountId) && _unsyncedSpaces[accountId].contains(f->spaceId()))
        {
            _unsyncedSpaces[accountId].remove(f->spaceId());
            emit unsyncedSpaceCountChanged(accountId, _unsyncedSpaces[accountId].count(), accountState->account()->spacesManager()->spacesCount());
        }

        f->journalDb()->setSelectiveSyncList(SyncJournalDb::SelectiveSyncBlackList, description.selectiveSyncBlackList);
        f->journalDb()->setSelectiveSyncList(SyncJournalDb::SelectiveSyncWhiteList, {QLatin1String("/")});

        _scheduler->enqueueFolder(f, SyncScheduler::Priority::High);
    }
    setSyncEnabled(true);
}

QString FolderMan::suggestSyncFolder(NewFolderType folderType, const QUuid &accountUuid)
{
    return FolderMan::instance()->findGoodPathForNewSyncFolder(QDir::homePath(), Theme::instance()->appName(), folderType, accountUuid);
}

bool FolderMan::prepareFolder(const QString &folder)
{
    if (!QFileInfo::exists(folder)) {
        if (!OC_ENSURE(QDir().mkpath(folder))) {
            return false;
        }
        // mac only
        FileSystem::setFolderMinimumPermissions(folder);
        // this is for windows - it sets up a desktop.ini file to handle the icon and deals with persmissions.
        Folder::prepareFolder(folder);
    }
    return true;
}

std::unique_ptr<FolderMan> FolderMan::createInstance()
{
    OC_ASSERT(!_instance);
    _instance = new FolderMan();
    return std::unique_ptr<FolderMan>(_instance);
}

} // namespace OCC
