/*
 * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
 * SPDX-License-Identifier: GPL-2.0-or-later
 */

#include "syncstatussummary.h"
#include "accountfwd.h"
#include "accountstate.h"
#include "folderman.h"
#include "navigationpanehelper.h"
#include "networkjobs.h"
#include "syncresult.h"
#include "tray/usermodel.h"

#ifdef BUILD_FILE_PROVIDER_MODULE
#include "gui/macOS/fileprovider.h"
#include "gui/macOS/fileproviderservice.h"
#include "gui/macOS/fileprovidersettingscontroller.h"
#endif

#include <theme.h>

namespace {

OCC::SyncResult::Status determineSyncStatus(const OCC::SyncResult &syncResult)
{
    const auto status = syncResult.status();

    if (status == OCC::SyncResult::Success || status == OCC::SyncResult::Problem) {
        if (syncResult.hasUnresolvedConflicts()) {
            return OCC::SyncResult::Problem;
        }
        return OCC::SyncResult::Success;
    } else if (status == OCC::SyncResult::SyncPrepare || status == OCC::SyncResult::Undefined) {
        return OCC::SyncResult::SyncRunning;
    }
    return status;
}

bool hasConfiguredSyncSource(const OCC::AccountStatePtr &accountState)
{
    if (!accountState) {
        return false;
    }
    
 #ifdef BUILD_FILE_PROVIDER_MODULE
    const auto account = accountState->account();
    const auto userIdAtHostWithPort = account->userIdAtHostWithPort();
    if (OCC::Mac::FileProviderSettingsController::instance()->vfsEnabledForAccount(userIdAtHostWithPort)) {
        return true;
    }
#endif

    for (const auto &folder : OCC::FolderMan::instance()->map()) {
        if (folder->accountState() != accountState.data()) {
            continue;
        }
        
        return true;
    }

    return false;
}
}

namespace OCC {

Q_LOGGING_CATEGORY(lcSyncStatusModel, "nextcloud.gui.syncstatusmodel", QtInfoMsg)

SyncStatusSummary::SyncStatusSummary(QObject *parent)
    : QObject(parent)
{
    const auto folderMan = FolderMan::instance();
    connect(folderMan, &FolderMan::folderListChanged, this, &SyncStatusSummary::onFolderListChanged);
    connect(folderMan, &FolderMan::folderSyncStateChange, this, &SyncStatusSummary::onFolderSyncStateChanged);
#ifdef BUILD_FILE_PROVIDER_MODULE
    connect(Mac::FileProvider::instance()->service(), &Mac::FileProviderService::syncStateChanged, this, &SyncStatusSummary::onFileProviderDomainSyncStateChanged);
#endif
}

bool SyncStatusSummary::reloadNeeded(AccountState *accountState) const
{
    if (_accountState.data() == accountState) {
        return false;
    }
    return true;
}

void SyncStatusSummary::load()
{
    const auto currentUser = UserModel::instance()->currentUser();
    if (!currentUser) {
        return;
    }

    loadForAccount(currentUser->accountState());
}

void SyncStatusSummary::loadForAccount(const AccountStatePtr &accountState)
{
    if (!accountState) {
        return;
    }

    setAccountState(accountState);
    clearFolderErrors();
    connectToFoldersProgress(FolderMan::instance()->map());
    initSyncState();
}

double SyncStatusSummary::syncProgress() const
{
    return _progress;
}

QUrl SyncStatusSummary::syncIcon() const
{
    return _syncIcon;
}

bool SyncStatusSummary::syncing() const
{
    return _isSyncing;
}

void SyncStatusSummary::onFolderListChanged(const OCC::Folder::Map &folderMap)
{
    connectToFoldersProgress(folderMap);
}

void SyncStatusSummary::markFolderAsError(const Folder *folder)
{
    _foldersWithErrors.insert(folder->alias());
}

void SyncStatusSummary::markFolderAsSuccess(const Folder *folder)
{
    _foldersWithErrors.erase(folder->alias());
}

bool SyncStatusSummary::folderErrors() const
{
    return !_foldersWithErrors.empty();
}

bool SyncStatusSummary::folderError(const Folder *folder) const
{
    return _foldersWithErrors.find(folder->alias()) != _foldersWithErrors.end();
}

void SyncStatusSummary::clearFolderErrors()
{
    _foldersWithErrors.clear();
}

void SyncStatusSummary::setSyncStateForFolder(const Folder *folder)
{
    if (_accountState && !_accountState->isConnected()) {
        setSyncing(false);
        setTotalFiles(0);
        setSyncStatusString(tr("Offline"));
        setSyncStatusDetailString("");
        if (_accountState->state() == AccountState::NeedToSignTermsOfService) {
            setSyncStatusDetailString(tr("You need to accept the terms of service"));
        }
        setSyncIcon(Theme::instance()->folderOffline());
        return;
    }

#ifdef Q_OS_MACOS
    if (folder->needsSandboxBookmark()) {
        markFolderAsError(folder);
        setSyncing(false);
        setTotalFiles(0);
        setSyncStatusString(tr("Reauthorization required"));
        setSyncStatusDetailString(tr("Please grant access to your sync folders"));
        setSyncIcon(Theme::instance()->error());
        setNeedsSandboxReapproval(true);
        return;
    }
#endif

    const auto state = determineSyncStatus(folder->syncResult());

    switch (state) {
    case SyncResult::Success:
    case SyncResult::SyncPrepare:
        markFolderAsSuccess(folder);
        break;
    case SyncResult::Error:
    case SyncResult::SetupError:
    case SyncResult::Problem:
    case SyncResult::Undefined:
        markFolderAsError(folder);
    case SyncResult::SyncRunning:
    case SyncResult::NotYetStarted:
    case SyncResult::Paused:
    case SyncResult::SyncAbortRequested:
        break;
    }

    updateNeedsSandboxReapproval();
    setSyncState(state);
}

void SyncStatusSummary::setSyncState(const SyncResult::Status state)
{
    if (_accountState && !_accountState->isConnected()) {
        setSyncing(false);
        setTotalFiles(0);
        setSyncStatusString(tr("Offline"));
        setSyncStatusDetailString("");
        setSyncIcon(Theme::instance()->offline());
        return;
    }

    switch (state) {
    case SyncResult::Success:
    case SyncResult::SyncPrepare:
    case SyncResult::Undefined:
        // `Undefined` reaches this point only via the file provider path, before the extension has
        // reported a concrete state (e.g. right after a fresh launch). It means "no status yet",
        // not "a sync failed", so render it as the optimistic resting state rather than the
        // misleading "Some files could not be synced!". This mirrors how the menu bar icon
        // (`ownCloudGui::slotComputeOverallSyncStatus`) and the per-account icons (`UserModel`)
        // already bucket `Undefined` as idle. The extension is additionally forced to report its
        // real state once the app connects. See https://github.com/nextcloud/desktop/issues/10053.
        //
        // Success should only be shown if all folders were fine
        if (!folderErrors()
#ifdef BUILD_FILE_PROVIDER_MODULE
            && _fileProviderDomainsWithErrors.empty()
#endif
        ) {
            setSyncing(false);
            setTotalFiles(0);
            setSyncStatusString(tr("All synced!"));
            setSyncStatusDetailString("");
            setSyncIcon(Theme::instance()->ok());
        }
        break;
    case SyncResult::Error:
    case SyncResult::SetupError:
        setSyncing(false);
        setTotalFiles(0);
        setSyncStatusString(tr("Some files couldn't be synced!"));
        setSyncStatusDetailString(tr("See below for errors"));
        setSyncIcon(Theme::instance()->error());
        break;
    case SyncResult::SyncRunning:
    case SyncResult::NotYetStarted:
        setSyncing(true);
        if (totalFiles() <= 0) {
            setSyncStatusString(tr("Checking folder changes"));
        } else {
            setSyncStatusString(tr("Syncing changes"));
        }
        setSyncStatusDetailString("");
        setSyncIcon(Theme::instance()->sync());
        break;
    case SyncResult::Paused:
    case SyncResult::SyncAbortRequested:
        setSyncing(false);
        setTotalFiles(0);
        setSyncStatusString(tr("Sync paused"));
        setSyncStatusDetailString("");
        setSyncIcon(Theme::instance()->pause());
        break;
    case SyncResult::Problem:
        setSyncing(false);
        setTotalFiles(0);
        setSyncStatusString(tr("Some files could not be synced!"));
        setSyncStatusDetailString(tr("See below for warnings"));
        setSyncIcon(Theme::instance()->warning());
        break;
    }
}

void SyncStatusSummary::onFolderSyncStateChanged(const Folder *folder)
{
    if (!folder) {
        return;
    }

    if (!_accountState || folder->accountState() != _accountState.data()) {
        return;
    }

    setSyncStateForFolder(folder);
}

#ifdef BUILD_FILE_PROVIDER_MODULE
void SyncStatusSummary::onFileProviderDomainSyncStateChanged(const AccountPtr &account, SyncResult::Status state)
{
    if (!_accountState || !_accountState->isConnected() || account != _accountState->account()) {
        return;
    }

    switch (state) {
    case SyncResult::Success:
    case SyncResult::SyncPrepare:
        // The domain is healthy again, so it no longer counts towards the "some files
        // couldn't be synced" header.
        _fileProviderDomainsWithErrors.erase(account->userIdAtHostWithPort());
        break;
    case SyncResult::Error:
    case SyncResult::SetupError:
    case SyncResult::Problem:
        // Mark this domain as failing so `setSyncState`'s `Success` branch correctly gates
        // the "All synced!" header on `_fileProviderDomainsWithErrors.empty()`. The previous
        // inversion erased the domain and forced `state = Success`, which is why the header
        // showed "Everything synchronized!" while the tray icon was red. See
        // https://github.com/nextcloud/desktop/issues/9598.
        _fileProviderDomainsWithErrors.insert(account->userIdAtHostWithPort());
        break;
    case SyncResult::SyncRunning:
    case SyncResult::NotYetStarted:
    case SyncResult::Paused:
    case SyncResult::SyncAbortRequested:
    case SyncResult::Undefined:
        // `Undefined` means the extension has not reported a concrete state yet (e.g. right after
        // launch). It is not an error, so leave `_fileProviderDomainsWithErrors` untouched: a
        // previously reported real error stays recorded, while a freshly configured domain simply
        // stays out of the error set. The matching `setSyncState` branch then renders it as the
        // optimistic resting state instead of the misleading warning. See
        // https://github.com/nextcloud/desktop/issues/10053.
        break;
    }

    setSyncState(state);
}
#endif

constexpr double calculateOverallPercent(
    qint64 totalFileCount, qint64 completedFile, qint64 totalSize, qint64 completedSize)
{
    int overallPercent = 0;
    if (totalFileCount > 0) {
        // Add one 'byte' for each file so the percentage is moving when deleting or renaming files
        overallPercent = qRound(double(completedSize + completedFile) / double(totalSize + totalFileCount) * 100.0);
    }
    overallPercent = qBound(0, overallPercent, 100);
    return overallPercent / 100.0;
}

void SyncStatusSummary::onFolderProgressInfo(const ProgressInfo &progress)
{
    const qint64 completedSize = progress.completedSize();
    const qint64 currentFile = progress.currentFile();
    const qint64 completedFile = progress.completedFiles();
    const qint64 totalSize = qMax(completedSize, progress.totalSize());
    const qint64 numFilesInProgress = qMax(currentFile, progress.totalFiles());

    if (_totalFiles <= 0 && numFilesInProgress > 0) {
        setSyncStatusString(tr("Syncing"));
    }

    setTotalFiles(numFilesInProgress);

    setSyncProgress(calculateOverallPercent(numFilesInProgress, completedFile, totalSize, completedSize));

    if (totalSize > 0) {
        const auto completedSizeString = Utility::octetsToString(completedSize);
        const auto totalSizeString = Utility::octetsToString(totalSize);

        if (progress.trustEta()) {
            setSyncStatusDetailString(
                tr("%1 of %2 · %3 left")
                    .arg(completedSizeString, totalSizeString)
                    .arg(Utility::durationToDescriptiveString1(progress.totalProgress().estimatedEta)));
        } else {
            setSyncStatusDetailString(tr("%1 of %2").arg(completedSizeString, totalSizeString));
        }
    }

    if (numFilesInProgress > 0) {
        setSyncStatusString(tr("Syncing file %1 of %2").arg(currentFile).arg(numFilesInProgress));
    }
}

void SyncStatusSummary::setSyncing(bool value)
{
    if (value == _isSyncing) {
        return;
    }

    _isSyncing = value;
    Q_EMIT syncingChanged();
}

void SyncStatusSummary::setTotalFiles(const qint64 value)
{
    if (value != _totalFiles) {
        _totalFiles = value;
        Q_EMIT totalFilesChanged();
    }
}

void SyncStatusSummary::setSyncProgress(double value)
{
    if (_progress == value) {
        return;
    }

    _progress = value;
    Q_EMIT syncProgressChanged();
}

void SyncStatusSummary::setSyncStatusString(const QString &value)
{
    if (_syncStatusString == value) {
        return;
    }

    _syncStatusString = value;
    Q_EMIT syncStatusStringChanged();
}

QString SyncStatusSummary::syncStatusString() const
{
    return _syncStatusString;
}

QString SyncStatusSummary::syncStatusDetailString() const
{
    return _syncStatusDetailString;
}

qint64 SyncStatusSummary::totalFiles() const
{
    return _totalFiles;
}

void SyncStatusSummary::setSyncIcon(const QUrl &value)
{
    if (_syncIcon == value) {
        return;
    }

    _syncIcon = value;
    Q_EMIT syncIconChanged();
}

void SyncStatusSummary::setSyncStatusDetailString(const QString &value)
{
    if (_syncStatusDetailString == value) {
        return;
    }

    _syncStatusDetailString = value;
    Q_EMIT syncStatusDetailStringChanged();
}

void SyncStatusSummary::connectToFoldersProgress(const Folder::Map &folderMap)
{
    for (const auto &folder : folderMap) {
        if (folder->accountState() == _accountState.data()) {
            connect(
                folder, &Folder::progressInfo, this, &SyncStatusSummary::onFolderProgressInfo, Qt::UniqueConnection);
        } else {
            disconnect(folder, &Folder::progressInfo, this, &SyncStatusSummary::onFolderProgressInfo);
        }
    }
}

void SyncStatusSummary::onIsConnectedChanged()
{
    setSyncStateToConnectedState();
}

void SyncStatusSummary::setSyncStateToConnectedState()
{
    setSyncing(false);
    setTotalFiles(0);
    setSyncStatusDetailString("");
    if (_accountState && !_accountState->isConnected()) {
        setSyncStatusString(tr("Offline"));
        setSyncIcon(Theme::instance()->folderOffline());
    } else if (!hasConfiguredSyncSource(_accountState)) {
        setSyncStatusString(tr("No synchronisation configured"));
        setSyncIcon(Theme::instance()->pause());
    } else {
        setSyncStatusString(tr("All synced!"));
        setSyncIcon(Theme::instance()->syncStatusOk());
    }
}

void SyncStatusSummary::setAccountState(AccountStatePtr accountState)
{
    if (!reloadNeeded(accountState.data())) {
        return;
    }
    if (_accountState) {
        disconnect(
            _accountState.data(), &AccountState::isConnectedChanged, this, &SyncStatusSummary::onIsConnectedChanged);
    }
    _accountState = accountState;
    connect(_accountState.data(), &AccountState::isConnectedChanged, this, &SyncStatusSummary::onIsConnectedChanged);
}

void SyncStatusSummary::initSyncState()
{
    auto syncStateFallbackNeeded = true;
    for (const auto &folder : FolderMan::instance()->map()) {
        if (!folder || folder->accountState() != _accountState.data()) {
            continue;
        }

        onFolderSyncStateChanged(folder);
        syncStateFallbackNeeded = false;
    }

#ifdef BUILD_FILE_PROVIDER_MODULE
    if (_accountState) {
        const auto account = _accountState->account();
        const auto userIdAtHostWithPort = account->userIdAtHostWithPort();

        if (Mac::FileProviderSettingsController::instance()->vfsEnabledForAccount(userIdAtHostWithPort)) {
            const auto lastKnownSyncState = Mac::FileProvider::instance()->service()->latestReceivedSyncStatusForAccount(account);
            onFileProviderDomainSyncStateChanged(account, lastKnownSyncState);
            syncStateFallbackNeeded = false;
        }
    }
#endif

    if (syncStateFallbackNeeded) {
        setSyncStateToConnectedState();
    }
}

bool SyncStatusSummary::needsSandboxReapproval() const
{
    return _needsSandboxReapproval;
}

void SyncStatusSummary::setNeedsSandboxReapproval(bool value)
{
    if (_needsSandboxReapproval == value) {
        return;
    }

    _needsSandboxReapproval = value;
    Q_EMIT needsSandboxReapprovalChanged();
}

void SyncStatusSummary::updateNeedsSandboxReapproval()
{
#ifdef Q_OS_MACOS
    for (const auto &folder : FolderMan::instance()->map()) {
        if (folder && folder->accountState() == _accountState.data() && folder->needsSandboxBookmark()) {
            setNeedsSandboxReapproval(true);
            return;
        }
    }
#endif
    setNeedsSandboxReapproval(false);
}
}
