#include "./syncthingconnection.h"
#include "./syncthingconfig.h"
#include "./syncthingconnectionsettings.h"
#include "./utils.h"

#ifdef LIB_SYNCTHING_CONNECTOR_CONNECTION_MOCKED
#include "./syncthingconnectionmockhelpers.h"
#endif

#include <c++utilities/conversion/stringconversion.h>

#include <QAuthenticator>
#include <QHostAddress>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonValue>
#include <QNetworkAccessManager>
#include <QNetworkInterface>
#include <QNetworkReply>
#include <QStringBuilder>
#include <QTimer>

#include <utility>

using namespace std;
using namespace ChronoUtilities;
using namespace ConversionUtilities;

namespace Data {

/*!
 * \brief Returns the QNetworkAccessManager instance used by SyncthingConnection instances.
 */
QNetworkAccessManager &networkAccessManager()
{
    static auto networkAccessManager = new QNetworkAccessManager;
    return *networkAccessManager;
}

/*!
 * \class SyncthingConnection
 * \brief The SyncthingConnection class allows Qt applications to access Syncthing via its REST API.
 * \remarks All requests are performed asynchronously.
 *
 * The first thing to do when working with that class is setting the URL to connect to and the API key
 * via the constructor or setSyncthingUrl() and setApiKey(). Credentials for the HTTP authentification
 * can be set via setCredentials() if not included in the URL.
 *
 * Requests can then be done via the request...() methods, eg. requestConfig(). This would emit the
 * newConfig() signal on success and the error() signal when an error occured. The other request...()
 * methods work in a similar way.
 *
 * However, usually it is best to simply call the connect() method. It will do all required requests
 * to populate dirInfo(), devInfo(), myId(), totalIncomingTraffic(), totalOutgoingTraffic() and all
 * the other variables. It will also use Syncthing's event API to listen for changes. The signals
 * newDirs() and newDevs() are can be used to know when dirInfo() and devInfo() become available.
 * Note that in this case the previous dirInfo()/devInfo() is invalidated.
 */

/*!
 * \brief Constructs a new instance ready to connect. To establish the connection, call connect().
 */
SyncthingConnection::SyncthingConnection(const QString &syncthingUrl, const QByteArray &apiKey, QObject *parent)
    : QObject(parent)
    , m_syncthingUrl(syncthingUrl)
    , m_apiKey(apiKey)
    , m_status(SyncthingStatus::Disconnected)
    , m_keepPolling(false)
    , m_abortingAllRequests(false)
    , m_abortingToReconnect(false)
    , m_requestCompletion(false)
    , m_lastEventId(0)
    , m_lastDiskEventId(0)
    , m_autoReconnectTries(0)
    , m_totalIncomingTraffic(unknownTraffic)
    , m_totalOutgoingTraffic(unknownTraffic)
    , m_totalIncomingRate(0.0)
    , m_totalOutgoingRate(0.0)
    , m_configReply(nullptr)
    , m_statusReply(nullptr)
    , m_connectionsReply(nullptr)
    , m_errorsReply(nullptr)
    , m_dirStatsReply(nullptr)
    , m_devStatsReply(nullptr)
    , m_eventsReply(nullptr)
    , m_versionReply(nullptr)
    , m_diskEventsReply(nullptr)
    , m_logReply(nullptr)
    , m_unreadNotifications(false)
    , m_hasConfig(false)
    , m_hasStatus(false)
    , m_hasEvents(false)
    , m_hasDiskEvents(false)
    , m_lastFileDeleted(false)
    , m_dirStatsAltered(false)
{
    m_trafficPollTimer.setInterval(SyncthingConnectionSettings::defaultTrafficPollInterval);
    m_trafficPollTimer.setTimerType(Qt::VeryCoarseTimer);
    m_trafficPollTimer.setSingleShot(true);
    QObject::connect(&m_trafficPollTimer, &QTimer::timeout, this, &SyncthingConnection::requestConnections);
    m_devStatsPollTimer.setInterval(SyncthingConnectionSettings::defaultDevStatusPollInterval);
    m_devStatsPollTimer.setTimerType(Qt::VeryCoarseTimer);
    m_devStatsPollTimer.setSingleShot(true);
    QObject::connect(&m_devStatsPollTimer, &QTimer::timeout, this, &SyncthingConnection::requestDeviceStatistics);
    m_errorsPollTimer.setInterval(SyncthingConnectionSettings::defaultErrorsPollInterval);
    m_errorsPollTimer.setTimerType(Qt::VeryCoarseTimer);
    m_errorsPollTimer.setSingleShot(true);
    QObject::connect(&m_errorsPollTimer, &QTimer::timeout, this, &SyncthingConnection::requestErrors);
    m_autoReconnectTimer.setTimerType(Qt::VeryCoarseTimer);
    m_autoReconnectTimer.setInterval(SyncthingConnectionSettings::defaultReconnectInterval);
    QObject::connect(&m_autoReconnectTimer, &QTimer::timeout, this, &SyncthingConnection::autoReconnect);

#ifdef LIB_SYNCTHING_CONNECTOR_CONNECTION_MOCKED
    setupTestData();
#endif
}

/*!
 * \brief Destroys the instance. Ongoing requests are aborted.
 */
SyncthingConnection::~SyncthingConnection()
{
    m_status = SyncthingStatus::BeingDestroyed;
    disconnect();
}

/*!
 * \brief Returns whether the currently assigned syncthingUrl() refers to the Syncthing instance on the local machine.
 */
bool SyncthingConnection::isLocal() const
{
    return ::Data::isLocal(QUrl(m_syncthingUrl));
}

/*!
 * \brief Returns the string representation of the specified \a status.
 */
QString SyncthingConnection::statusText(SyncthingStatus status)
{
    switch (status) {
    case SyncthingStatus::Disconnected:
        return tr("disconnected");
    case SyncthingStatus::Reconnecting:
        return tr("reconnecting");
    case SyncthingStatus::Idle:
        return tr("connected");
    case SyncthingStatus::Scanning:
        return tr("connected, scanning");
    case SyncthingStatus::Paused:
        return tr("connected, paused");
    case SyncthingStatus::Synchronizing:
        return tr("connected, synchronizing");
    case SyncthingStatus::OutOfSync:
        return tr("connected, out of sync");
    default:
        return tr("unknown");
    }
}

/*!
 * \brief Returns whether there is at least one directory out-of-sync.
 */
bool SyncthingConnection::hasOutOfSyncDirs() const
{
    for (const SyncthingDir &dir : m_dirs) {
        if (dir.status == SyncthingDirStatus::OutOfSync) {
            return true;
        }
    }
    return false;
}

/*!
 * \brief Sets all polling intervals (traffic, device statistics, errors) to 0 so polling is disabled.
 * \remarks Does not affect the auto-reconnect and does not affect the *long* polling for the event API.
 */
void SyncthingConnection::disablePolling()
{
    setTrafficPollInterval(0);
    setDevStatsPollInterval(0);
    setErrorsPollInterval(0);
}

/*!
 * \brief Connects asynchronously to Syncthing. Does nothing if already connected.
 *
 * Use this to connect the first time or to connect to the same Syncthing instance again or to ensure
 * the connection to the currently configured instance is established. Use reconnect() to connect to
 * a different instance.
 *
 * \remarks Does not clear data from a previous connection (except error items). Use reconnect() if that
 *          is required.
 */
void SyncthingConnection::connect()
{
    // reset auto-reconnect
    m_autoReconnectTimer.stop();
    m_autoReconnectTries = 0;

    // skip if already connected (see reconnect() to force reconnecting)
    if (isConnected()) {
        return;
    }

    // reset status
    m_abortingToReconnect = m_hasConfig = m_hasStatus = m_hasEvents = m_hasDiskEvents = false;

    // check configuration
    if (m_apiKey.isEmpty() || m_syncthingUrl.isEmpty()) {
        emit error(tr("Connection configuration is insufficient."), SyncthingErrorCategory::OverallConnection, QNetworkReply::NoError);
        setStatus(SyncthingStatus::Disconnected);
        return;
    }

    // start by requesting config and status; if both are available request further info and events
    requestConfig();
    requestStatus();
    m_keepPolling = true;
}

/*!
 * \brief Applies the specified configuration and tries to reconnect via reconnect() if properties requiring reconnect
 *        to take effect have changed.
 * \remarks The expected SSL errors of the specified configuration are updated accordingly.
 */
void SyncthingConnection::connect(SyncthingConnectionSettings &connectionSettings)
{
    if (applySettings(connectionSettings) || !isConnected()) {
        reconnect();
    }
}

/*!
 * \brief Connects in \a milliSeconds. Useful to "schedule" another attempt in case of a failure.
 * \remarks Does nothing if the connection attempt would happen anyways though auto-reconnect.
 */
void SyncthingConnection::connectLater(int milliSeconds)
{
    // skip if conneting via auto-reconnect anyways
    if (autoReconnectInterval() > 0 && milliSeconds > autoReconnectInterval()) {
        return;
    }
    QTimer::singleShot(milliSeconds, this, static_cast<void (SyncthingConnection::*)(void)>(&SyncthingConnection::connect));
}

/*!
 * \brief Disconnects. Does nothing if not connected.
 */
void SyncthingConnection::disconnect()
{
    m_abortingToReconnect = m_keepPolling = false;
    m_autoReconnectTries = 0;
    abortAllRequests();
}

/*!
 * \brief Aborts status-relevant, pending requests.
 * \remarks Status-relevant means that requests for tiggering actions like rescan() or restart() are excluded. requestQrCode() does not
 *          contribute to the status as well and is excluded as well.
 */
void SyncthingConnection::abortAllRequests()
{
    m_abortingAllRequests = true;
    if (m_configReply) {
        m_configReply->abort();
    }
    if (m_statusReply) {
        m_statusReply->abort();
    }
    if (m_connectionsReply) {
        m_connectionsReply->abort();
    }
    if (m_errorsReply) {
        m_errorsReply->abort();
    }
    if (m_dirStatsReply) {
        m_dirStatsReply->abort();
    }
    if (m_devStatsReply) {
        m_devStatsReply->abort();
    }
    if (m_eventsReply) {
        m_eventsReply->abort();
    }
    if (m_versionReply) {
        m_versionReply->abort();
    }
    if (m_diskEventsReply) {
        m_diskEventsReply->abort();
    }
    if (m_logReply) {
        m_logReply->abort();
    }
    for (auto *const reply : m_otherReplies) {
        reply->abort();
    }

    m_abortingAllRequests = false;
    handleAdditionalRequestCanceled();
}

/*!
 * \brief Disconnects if connected, then (re-)connects asynchronously.
 * \remarks
 * - Clears the currently cached configuration.
 * - This explicit request to reconnect will reset the autoReconnectTries().
 */
void SyncthingConnection::reconnect()
{
    // reset reconnect timer
    m_autoReconnectTimer.stop();
    m_autoReconnectTries = 0;

    // reset variables to track connection progress
    // note: especially resetting events is important as it influences the subsequent hasPendingRequests() call
    m_hasConfig = m_hasStatus = m_hasEvents = m_hasDiskEvents = false;

    // reconnect right now if no pending requests to be aborted
    if (!hasPendingRequests()) {
        continueReconnecting();
        return;
    }

    // abort pending requests before connecting again
    m_keepPolling = m_abortingToReconnect = true;
    abortAllRequests();
}

/*!
 * \brief Applies the specified configuration and tries to reconnect via reconnect().
 * \remarks The expected SSL errors of the specified configuration are updated accordingly.
 */
void SyncthingConnection::reconnect(SyncthingConnectionSettings &connectionSettings)
{
    applySettings(connectionSettings);
    reconnect();
}

/*!
 * \brief Reconnects after the specified number of \a milliSeconds.
 */
void SyncthingConnection::reconnectLater(int milliSeconds)
{
    QTimer::singleShot(milliSeconds, this, static_cast<void (SyncthingConnection::*)(void)>(&SyncthingConnection::reconnect));
}

/*!
 * \brief Internally called to reconnect; ensures currently cached config is cleared.
 */
void SyncthingConnection::continueReconnecting()
{
    // notify that we're about to invalidate the configuration if not already invalidated anyways
    const auto isConfigInvalidated = m_rawConfig.isEmpty();
    if (!isConfigInvalidated) {
        emit newConfig(m_rawConfig = QJsonObject());
    }

    // cleanup information from previous connection
    m_keepPolling = true;
    m_abortingToReconnect = false;
    m_lastEventId = 0;
    m_lastDiskEventId = 0;
    m_configDir.clear();
    m_myId.clear();
    m_totalIncomingTraffic = unknownTraffic;
    m_totalOutgoingTraffic = unknownTraffic;
    m_totalIncomingRate = 0.0;
    m_totalOutgoingRate = 0.0;
    emit trafficChanged(unknownTraffic, unknownTraffic);
    m_unreadNotifications = false;
    m_hasConfig = false;
    m_hasStatus = false;
    m_hasEvents = false;
    m_hasDiskEvents = false;
    m_dirs.clear();
    m_devs.clear();
    m_lastConnectionsUpdate = DateTime();
    m_lastFileTime = DateTime();
    m_lastErrorTime = DateTime();
    m_startTime = DateTime();
    m_lastFileName.clear();
    m_lastFileDeleted = false;
    m_syncthingVersion.clear();
    m_dirStatsAltered = false;
    emit dirStatisticsChanged();

    // notify that the configuration has been invalidated
    if (!isConfigInvalidated) {
        emit newConfigApplied();
    }

    if (m_apiKey.isEmpty() || m_syncthingUrl.isEmpty()) {
        emit error(tr("Connection configuration is insufficient."), SyncthingErrorCategory::OverallConnection, QNetworkReply::NoError);
        setStatus(SyncthingStatus::Disconnected);
        return;
    }

    requestConfig();
    requestStatus();

    setStatus(SyncthingStatus::Reconnecting);
}

/*!
 * \brief Reads results of requestConfig() and requestStatus().
 * \remarks Called in readConfig() or readStatus() to conclude reading parts requiring config *and* status
 *          being available. Does nothing if this is not the case (yet).
 */
void SyncthingConnection::concludeReadingConfigAndStatus()
{
    if (!m_hasConfig || !m_hasStatus) {
        return;
    }

    readDevs(m_rawConfig.value(QLatin1String("devices")).toArray());
    readDirs(m_rawConfig.value(QLatin1String("folders")).toArray());
    emit newConfigApplied();

    continueConnecting();
}

/*!
 * \brief Sets the state from (re)connecting to Syncthing's actual state if polling but there are no more pending requests.
 * \remarks Called by read...() handlers for requests started in continueConnecting().
 * \sa hasPendingRequests()
 */
void SyncthingConnection::concludeConnection()
{
    if (!m_keepPolling || hasPendingRequests()) {
        return;
    }
    setStatus(SyncthingStatus::Idle);
    emitDirStatisticsChanged();
}

/*!
 * \brief Connects increasing the auto-reconnect tries.
 * \remarks Called via m_autoReconnectTimer.
 */
void SyncthingConnection::autoReconnect()
{
    const auto tmp = m_autoReconnectTries;
    connect();
    m_autoReconnectTries = tmp + 1;
}

/*!
 * \brief Returns the directory info object for the directory with the specified ID.
 * \returns Returns a pointer to the object or nullptr if not found.
 * \remarks The returned object becomes invalid when the newDirs() signal is emitted or the connection is destroyed.
 */
SyncthingDir *SyncthingConnection::findDirInfo(const QString &dirId, int &row)
{
    row = 0;
    for (SyncthingDir &d : m_dirs) {
        if (d.id == dirId) {
            return &d;
        }
        ++row;
    }
    return nullptr;
}

/*!
 * \brief Returns the directory info object for the directory with the ID stored in the specified \a object with the specified \a key.
 */
SyncthingDir *SyncthingConnection::findDirInfo(QLatin1String key, const QJsonObject &object, int *row)
{
    const auto dirId(object.value(key).toString());
    if (dirId.isEmpty()) {
        return nullptr;
    }
    int dummyRow;
    auto &rowRef(row ? *row : dummyRow);
    return findDirInfo(dirId, rowRef);
}

/*!
 * \brief Returns the directory info object for the directory with the specified \a path.
 *
 * If a corresponding Syncthing directory could be found, \a relativePath is set to the path of the item relative
 * to the location of the corresponding Syncthing directory.
 *
 * \returns Returns a pointer to the object or nullptr if not found.
 * \remarks The returned object becomes invalid when the newDirs() signal is emitted or the connection is destroyed.
 */
SyncthingDir *SyncthingConnection::findDirInfoByPath(const QString &path, QString &relativePath, int &row)
{
    row = 0;
    for (SyncthingDir &dir : m_dirs) {
        if (path == dir.pathWithoutTrailingSlash()) {
            relativePath.clear();
            return &dir;
        } else if (path.startsWith(dir.path)) {
            relativePath = path.mid(dir.path.size());
            return &dir;
        }
        ++row;
    }
    return nullptr;
}

/*!
 * \brief Appends a directory info object with the specified \a dirId to \a dirs.
 *
 * If such an object already exists, it is recycled by moving it to \a dirs.
 * Otherwise a new, empty object is created.
 *
 * \returns Returns the directory info object or nullptr if \a dirId is invalid.
 */
SyncthingDir *SyncthingConnection::addDirInfo(std::vector<SyncthingDir> &dirs, const QString &dirId)
{
    if (dirId.isEmpty()) {
        return nullptr;
    }
    int row;
    if (auto *const existingDirInfo = findDirInfo(dirId, row)) {
        dirs.emplace_back(move(*existingDirInfo));
    } else {
        dirs.emplace_back(dirId);
    }
    return &dirs.back();
}

/*!
 * \brief Returns the device info object for the device with the specified ID.
 * \returns Returns a pointer to the object or nullptr if not found.
 * \remarks The returned object becomes invalid when the newConfig() signal is emitted or the connection is destroyed.
 */
SyncthingDev *SyncthingConnection::findDevInfo(const QString &devId, int &row)
{
    row = 0;
    for (SyncthingDev &d : m_devs) {
        if (d.id == devId) {
            return &d;
        }
        ++row;
    }
    return nullptr;
}

/*!
 * \brief Returns the device info object for the first device with the specified name.
 * \returns Returns a pointer to the object or nullptr if not found.
 * \remarks The returned object becomes invalid when the newConfig() signal is emitted or the connection is destroyed.
 */
SyncthingDev *SyncthingConnection::findDevInfoByName(const QString &devName, int &row)
{
    row = 0;
    for (SyncthingDev &d : m_devs) {
        if (d.name == devName) {
            return &d;
        }
        ++row;
    }
    return nullptr;
}

/*!
 * \brief Returns all directory IDs for the current configuration.
 * \remarks Computed by looping dirInfo().
 */
QStringList SyncthingConnection::directoryIds() const
{
    return ids(m_dirs);
}

/*!
 * \brief Returns all device IDs for the current configuration.
 * \remarks Computed by looping devInfo().
 */
QStringList SyncthingConnection::deviceIds() const
{
    return ids(m_devs);
}

/*!
 * \brief Returns the device name for the specified \a deviceId if known; otherwise returns just the \a deviceId itself.
 */
QString SyncthingConnection::deviceNameOrId(const QString &deviceId) const
{
    for (const auto &dev : devInfo()) {
        if (dev.id == deviceId) {
            return dev.name;
        }
    }
    return deviceId;
}

/*!
 * \brief Returns the number of devices Syncthing is currently connected to.
 * \remarks Computed by looping devInfo().
 */
std::vector<const SyncthingDev *> SyncthingConnection::connectedDevices() const
{
    std::vector<const SyncthingDev *> connectedDevs;
    connectedDevs.reserve(devInfo().size());
    for (const SyncthingDev &dev : devInfo()) {
        if (dev.isConnected()) {
            connectedDevs.emplace_back(&dev);
        }
    }
    return connectedDevs;
}

/*!
 * \brief Appends a device info object with the specified \a devId to \a devs.
 *
 * If such an object already exists, it is recycled by moving it to \a devs.
 * Otherwise a new, empty object is created.
 *
 * \returns Returns the device info object or nullptr if \a devId is invalid.
 */
SyncthingDev *SyncthingConnection::addDevInfo(std::vector<SyncthingDev> &devs, const QString &devId)
{
    if (devId.isEmpty()) {
        return nullptr;
    }
    int row;
    if (SyncthingDev *const existingDevInfo = findDevInfo(devId, row)) {
        devs.emplace_back(move(*existingDevInfo));
    } else {
        devs.emplace_back(devId);
    }
    return &devs.back();
}

/*!
 * \brief Continues connecting if both - config and status - have been parsed yet and continuous polling is enabled.
 */
void SyncthingConnection::continueConnecting()
{
    // skip if config and status are missing or we're not supposed to actually connect
    if (!m_keepPolling || !m_hasConfig || !m_hasStatus) {
        return;
    }

    // read additional information (beside config and status)
    // FIXME: make those requests configurable (eg. flag enum)
    requestConnections();
    requestDirStatistics();
    requestDeviceStatistics();
    requestErrors();
    requestVersion();
    for (const SyncthingDir &dir : m_dirs) {
        requestDirStatus(dir.id);
        if (!m_requestCompletion) {
            continue;
        }
        for (const QString &devId : dir.deviceIds) {
            requestCompletion(devId, dir.id);
        }
    }

    // poll for events
    m_lastEventId = m_lastDiskEventId = 0;
    requestEvents();
    requestDiskEvents();
}

/*!
 * \brief Locates and loads the (self-signed) certificate used by the Syncthing GUI.
 * \remarks
 *  - Ensures any previous certificates are cleared in any case.
 *  - Emits error() when an error occurs.
 *  - Loading the certificate is only possible if the connection object is configured
 *    to connect to the locally running Syncthing instance. Otherwise this method will
 *    only do the cleanup of previous certificates but not emit any errors.
 * \returns Returns whether a certificate could be loaded.
 */
bool SyncthingConnection::loadSelfSignedCertificate()
{
    // ensure current exceptions for self-signed certificates are cleared
    m_expectedSslErrors.clear();

    // not required when not using secure connection
    const QUrl syncthingUrl(m_syncthingUrl);
    if (!syncthingUrl.scheme().endsWith(QChar('s'))) {
        return false;
    }

    // only possible if the Syncthing instance is running on the local machine
    if (!::Data::isLocal(syncthingUrl)) {
        return false;
    }

    // find cert
    const QString certPath = !m_configDir.isEmpty() ? (m_configDir + QStringLiteral("/https-cert.pem")) : SyncthingConfig::locateHttpsCertificate();
    if (certPath.isEmpty()) {
        emit error(tr("Unable to locate certificate used by Syncthing."), SyncthingErrorCategory::OverallConnection, QNetworkReply::NoError);
        return false;
    }
    // add exception
    const QList<QSslCertificate> certs = QSslCertificate::fromPath(certPath);
    if (certs.isEmpty()) {
        emit error(tr("Unable to load certificate used by Syncthing."), SyncthingErrorCategory::OverallConnection, QNetworkReply::NoError);
        return false;
    }
    const QSslCertificate &cert = certs.at(0);
    m_expectedSslErrors.reserve(4);
    m_expectedSslErrors << QSslError(QSslError::UnableToGetLocalIssuerCertificate, cert) << QSslError(QSslError::UnableToVerifyFirstCertificate, cert)
                        << QSslError(QSslError::SelfSignedCertificate, cert) << QSslError(QSslError::HostNameMismatch, cert);
    return true;
}

/*!
 * \brief Applies the specified configuration.
 * \remarks
 * - The expected SSL errors of the specified configuration are updated accordingly.
 * - The configuration is not used instantly. It will be used on the next reconnect.
 * \returns Returns whether at least one property requiring a reconnect to take effect has changed.
 * \sa reconnect()
 */
bool SyncthingConnection::applySettings(SyncthingConnectionSettings &connectionSettings)
{
    bool reconnectRequired = false;
    if (syncthingUrl() != connectionSettings.syncthingUrl) {
        setSyncthingUrl(connectionSettings.syncthingUrl);
        reconnectRequired = true;
    }
    if (apiKey() != connectionSettings.apiKey) {
        setApiKey(connectionSettings.apiKey);
        reconnectRequired = true;
    }
    if ((connectionSettings.authEnabled && (user() != connectionSettings.userName || password() != connectionSettings.password))
        || (!connectionSettings.authEnabled && (!user().isEmpty() || !password().isEmpty()))) {
        if (connectionSettings.authEnabled) {
            setCredentials(connectionSettings.userName, connectionSettings.password);
        } else {
            setCredentials(QString(), QString());
        }
        reconnectRequired = true;
    }
    if (connectionSettings.expectedSslErrors.isEmpty()) {
        const bool previouslyHadExpectedSslErrors = !expectedSslErrors().isEmpty();
        const bool ok = loadSelfSignedCertificate();
        connectionSettings.expectedSslErrors = expectedSslErrors();
        if (ok || (previouslyHadExpectedSslErrors && !ok)) {
            reconnectRequired = true;
        }
    } else if (expectedSslErrors() != connectionSettings.expectedSslErrors) {
        m_expectedSslErrors = connectionSettings.expectedSslErrors;
        reconnectRequired = true;
    }

    setTrafficPollInterval(connectionSettings.trafficPollInterval);
    setDevStatsPollInterval(connectionSettings.devStatsPollInterval);
    setErrorsPollInterval(connectionSettings.errorsPollInterval);
    setAutoReconnectInterval(connectionSettings.reconnectInterval);

    return reconnectRequired;
}

/*!
 * \brief Sets the connection status. Ensures statusChanged() is emitted.
 * \param status Specifies the status; should be either SyncthingStatus::Disconnected, SyncthingStatus::Reconnecting, or
 *        SyncthingStatus::Idle. There is no use in specifying other values such as SyncthingStatus::Synchronizing as
 *        these are determined automatically within the method.
 */
void SyncthingConnection::setStatus(SyncthingStatus status)
{
    if (m_status == SyncthingStatus::BeingDestroyed) {
        return;
    }
    switch (status) {
    case SyncthingStatus::Disconnected:
        // disable (long) polling
        m_keepPolling = false;
        FALLTHROUGH;
    case SyncthingStatus::Reconnecting:
        m_devStatsPollTimer.stop();
        m_trafficPollTimer.stop();
        m_errorsPollTimer.stop();
        break;
    default:
        // reset reconnect tries
        m_autoReconnectTries = 0;

        // check whether at least one directory is scanning or synchronizing
        bool scanning = false;
        bool synchronizing = false;
        for (const SyncthingDir &dir : m_dirs) {
            switch (dir.status) {
            case SyncthingDirStatus::Synchronizing:
                synchronizing = true;
                break;
            case SyncthingDirStatus::Scanning:
                scanning = true;
                break;
            default:;
            }
        }

        if (synchronizing) {
            status = SyncthingStatus::Synchronizing;
        } else if (scanning) {
            status = SyncthingStatus::Scanning;
        } else {
            // check whether at least one device is paused
            bool paused = false;
            for (const SyncthingDev &dev : m_devs) {
                if (dev.paused) {
                    paused = true;
                    break;
                }
            }
            if (paused) {
                status = SyncthingStatus::Paused;
            } else {
                status = SyncthingStatus::Idle;
            }
        }
    }
    if (m_status != status) {
        emit statusChanged(m_status = status);
    }
}

/*!
 * \brief Interanlly called to emit the notification with the specified \a message.
 * \remarks Ensures the status is updated and the unread notifications flag is set.
 */
void SyncthingConnection::emitNotification(DateTime when, const QString &message)
{
    m_unreadNotifications = true;
    emit newNotification(when, message);
    recalculateStatus();
}

/*!
 * \brief Internally called to emit a JSON parsing error.
 * \remarks Since in this case the reply has already been read, its response must be passed as extra argument.
 */
void SyncthingConnection::emitError(const QString &message, const QJsonParseError &jsonError, QNetworkReply *reply, const QByteArray &response)
{
    emit error(message % jsonError.errorString() % QChar(' ') % QChar('(') % tr("at offset %1").arg(jsonError.offset) % QChar(')'),
        SyncthingErrorCategory::Parsing, QNetworkReply::NoError, reply->request(), response);
}

/*!
 * \brief Internally called to emit a network error (server replied error code are connection or server could not be reached at all).
 */
void SyncthingConnection::emitError(const QString &message, SyncthingErrorCategory category, QNetworkReply *reply)
{
    emit error(message + reply->errorString(), category, reply->error(), reply->request(), reply->bytesAvailable() ? reply->readAll() : QByteArray());
}

/*!
 * \brief Internally called to emit myIdChanged() signal.
 */
void SyncthingConnection::emitMyIdChanged(const QString &newId)
{
    if (newId.isEmpty() || m_myId == newId) {
        return;
    }

    // adjust device status
    int row = 0;
    for (SyncthingDev &dev : m_devs) {
        if (dev.id == newId) {
            if (dev.status != SyncthingDevStatus::OwnDevice) {
                dev.status = SyncthingDevStatus::OwnDevice;
                emit devStatusChanged(dev, row);
            }
        } else if (dev.status == SyncthingDevStatus::OwnDevice) {
            dev.status = SyncthingDevStatus::Unknown;
            emit devStatusChanged(dev, row);
        }
        ++row;
    }

    emit myIdChanged(m_myId = newId);
}

/*!
 * \brief Internally called to emit dirStatisticsChanged() event.
 */
void SyncthingConnection::emitDirStatisticsChanged()
{
    if (m_dirStatsAltered) {
        m_dirStatsAltered = false;
        emit dirStatisticsChanged();
    }
}

/*!
 * \brief Internally called to handle a fatal error when reading config (dirs/devs), status and events.
 */
void SyncthingConnection::handleFatalConnectionError()
{
    setStatus(SyncthingStatus::Disconnected);
    abortAllRequests();
    if (m_autoReconnectTimer.interval()) {
        m_autoReconnectTimer.start();
    }
}

/*!
 * \brief Handles cancelation of additional requests done via continueConnecting() method.
 */
void SyncthingConnection::handleAdditionalRequestCanceled()
{
    // postpone handling if there are still other requests pending
    if (hasPendingRequests()) {
        return;
    }
    if (m_abortingToReconnect) {
        // if reconnection flag is set, instantly etstablish a new connection ...
        continueReconnecting();
    } else {
        // ... otherwise declare we're disconnected if that was the last pending request
        setStatus(SyncthingStatus::Disconnected);
    }
}

/*!
 * \brief Internally called to recalculate the overall connection status, eg. after the status of a directory
 *        changed.
 * \remarks
 * - This is achieved by simply setting the status to idle. setStatus() will calculate the specific status.
 * - If not connected, this method does nothing. This is important, because when this method is called when
 *   establishing a connection (and the status is hence still disconnected) timers for polling would be
 *   killed.
 */
void SyncthingConnection::recalculateStatus()
{
    if (isConnected()) {
        setStatus(SyncthingStatus::Idle);
    }
}

/*!
 * \fn SyncthingConnection::newConfig()
 * \brief Indicates new configuration (dirs, devs, ...) is available.
 * \remarks
 * - Configuration is requested automatically when connecting.
 * - Previous directories (and directory info objects!) are invalidated.
 * - Previous devices (and device info objects!) are invalidated.
 */

/*!
 * \fn SyncthingConnection::newDirs()
 * \brief Indicates new directories are available.
 * \remarks Always emitted after newConfig() as soon as new directory info objects become available.
 */

/*!
 * \fn SyncthingConnection::newDevices()
 * \brief Indicates new devices are available.
 * \remarks Always emitted after newConfig() as soon as new device info objects become available.
 */

/*!
 * \fn SyncthingConnection::newEvents()
 * \brief Indicates new events (dir status changed, ...) are available.
 * \remarks New events are automatically polled when connected.
 */

/*!
 * \fn SyncthingConnection::dirStatusChanged()
 * \brief Indicates the status of the specified \a dir changed.
 */

/*!
 * \fn SyncthingConnection::devStatusChanged()
 * \brief Indicates the status of the specified \a dev changed.
 */

/*!
 * \fn SyncthingConnection::downloadProgressChanged()
 * \brief Indicates the download progress changed.
 */

/*!
 * \fn SyncthingConnection::newNotification()
 * \brief Indicates a new Syncthing notification is available.
 */

/*!
 * \fn SyncthingConnection::newDevAvailable()
 * \brief Indicates another device wants to talk to us.
 */

/*!
 * \fn SyncthingConnection::newDirAvailable()
 * \brief Indicates a device wants to share an so far unknown directory with us.
 * \remarks \a dev might be nullptr. In this case the device which wants to share the directory is unknown as well.
 */

/*!
 * \fn SyncthingConnection::error()
 * \brief Indicates a request (for configuration, events, ...) failed.
 */

/*!
 * \fn SyncthingConnection::statusChanged()
 * \brief Indicates the status of the connection changed.
 */

/*!
 * \fn SyncthingConnection::configDirChanged()
 * \brief Indicates the Syncthing home/configuration directory changed.
 */

/*!
 * \fn SyncthingConnection::myIdChanged()
 * \brief Indicates ID of the own Syncthing device changed.
 */

/*!
 * \fn SyncthingConnection::trafficChanged()
 * \brief Indicates totalIncomingTraffic() or totalOutgoingTraffic() has changed.
 */

/*!
 * \fn SyncthingConnection::newConfigTriggered()
 * \brief Indicates a new configuration has posted sucessfully via postConfig().
 * \remarks In contrast to newConfig(), this signal is only emitted for configuration changes internally posted via postConfig().
 */

/*!
 * \fn SyncthingConnection::rescanTriggered()
 * \brief Indicates a rescan has been triggered sucessfully.
 * \remarks Only emitted for rescans triggered internally via rescan() or rescanAll().
 */

/*!
 * \fn SyncthingConnection::pauseTriggered()
 * \brief Indicates a device has been paused sucessfully.
 * \remarks Only emitted for pausing triggered internally via pause() or pauseAll().
 */

/*!
 * \fn SyncthingConnection::resumeTriggered()
 * \brief Indicates a device has been resumed sucessfully.
 * \remarks Only emitted for resuming triggered internally via resume() or resumeAll().
 */

/*!
 * \fn SyncthingConnection::restartTriggered()
 * \brief Indicates a restart has been successfully triggered via restart().
 */
} // namespace Data
