/*
 * Bittorrent Client using Qt and libtorrent.
 * Copyright (C) 2015-2025  Vladimir Golovnev <glassez@yandex.ru>
 * Copyright (C) 2006  Christophe Dumez <chris@qbittorrent.org>
 *
 * 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.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 *
 * In addition, as a special exception, the copyright holders give permission to
 * link this program with the OpenSSL project's "OpenSSL" library (or with
 * modified versions of it that use the same license as the "OpenSSL" library),
 * and distribute the linked executables. You must obey the GNU General Public
 * License in all respects for all of the code used other than "OpenSSL".  If you
 * modify file(s), you may extend this exception to your version of the file(s),
 * but you are not obligated to do so. If you do not wish to do so, delete this
 * exception statement from your version.
 */

#include "sessionimpl.h"

#include <algorithm>
#include <concepts>
#include <cstdint>
#include <ctime>
#include <ranges>
#include <string>

#ifdef Q_OS_WIN
#include <windows.h>
#include <wincrypt.h>
#include <iphlpapi.h>
#endif

#include <boost/asio/ip/tcp.hpp>

#include <libtorrent/add_torrent_params.hpp>
#include <libtorrent/address.hpp>
#include <libtorrent/alert_types.hpp>
#include <libtorrent/error_code.hpp>
#include <libtorrent/extensions/smart_ban.hpp>
#include <libtorrent/extensions/ut_metadata.hpp>
#include <libtorrent/extensions/ut_pex.hpp>
#include <libtorrent/ip_filter.hpp>
#include <libtorrent/magnet_uri.hpp>
#include <libtorrent/session.hpp>
#include <libtorrent/session_stats.hpp>
#include <libtorrent/session_status.hpp>
#include <libtorrent/torrent_info.hpp>
#include <libtorrent/version.hpp>

#include <QDateTime>
#include <QDeadlineTimer>
#include <QDebug>
#include <QDir>
#include <QFuture>
#include <QHostAddress>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonValue>
#include <QMutexLocker>
#include <QNetworkAddressEntry>
#include <QNetworkInterface>
#include <QPromise>
#include <QRegularExpression>
#include <QString>
#include <QThread>
#include <QTimer>
#include <QUuid>

#include "base/algorithm.h"
#include "base/freediskspacechecker.h"
#include "base/global.h"
#include "base/logger.h"
#include "base/net/downloadmanager.h"
#include "base/net/proxyconfigurationmanager.h"
#include "base/preferences.h"
#include "base/profile.h"
#include "base/unicodestrings.h"
#include "base/utils/fs.h"
#include "base/utils/io.h"
#include "base/utils/net.h"
#include "base/utils/number.h"
#include "base/utils/random.h"
#include "base/utils/string.h"
#include "base/version.h"
#include "bandwidthscheduler.h"
#include "bencoderesumedatastorage.h"
#include "customstorage.h"
#include "dbresumedatastorage.h"
#include "downloadpriority.h"
#include "extensiondata.h"
#include "filesearcher.h"
#include "filterparserthread.h"
#include "loadtorrentparams.h"
#include "lttypecast.h"
#include "nativesessionextension.h"
#include "peer_blacklist.hpp"
#include "peer_filter_session_plugin.hpp"
#include "peer_shadowban_plugin.hpp"
#include "portforwarderimpl.h"
#include "resumedatastorage.h"
#include "torrentcontentremover.h"
#include "torrentdescriptor.h"
#include "torrentimpl.h"
#include "tracker.h"
#include "trackerentry.h"
#include "trackerentrystatus.h"

using namespace std::chrono_literals;
using namespace BitTorrent;

const Path CATEGORIES_FILE_NAME {u"categories.json"_s};
const Path ADDITIONAL_TRACKERS_FROM_URL_FILE_NAME {u"additional_trackers_from_url.txt"_s};
const int MAX_PROCESSING_RESUMEDATA_COUNT = 50;
const std::chrono::seconds FREEDISKSPACE_CHECK_TIMEOUT = 30s;

namespace
{
    const char PEER_ID[] = "qB";
    const auto USER_AGENT = QStringLiteral("qBittorrent Enhanced/" QBT_VERSION_2);
    const QString DEFAULT_DHT_BOOTSTRAP_NODES = u"dht.libtorrent.org:25401, dht.transmissionbt.com:6881, router.bittorrent.com:6881"_s;

    void torrentQueuePositionUp(const lt::torrent_handle &handle)
    {
        try
        {
            handle.queue_position_up();
        }
        catch (const std::exception &exc)
        {
            qDebug() << Q_FUNC_INFO << " fails: " << exc.what();
        }
    }

    void torrentQueuePositionDown(const lt::torrent_handle &handle)
    {
        try
        {
            handle.queue_position_down();
        }
        catch (const std::exception &exc)
        {
            qDebug() << Q_FUNC_INFO << " fails: " << exc.what();
        }
    }

    void torrentQueuePositionTop(const lt::torrent_handle &handle)
    {
        try
        {
            handle.queue_position_top();
        }
        catch (const std::exception &exc)
        {
            qDebug() << Q_FUNC_INFO << " fails: " << exc.what();
        }
    }

    void torrentQueuePositionBottom(const lt::torrent_handle &handle)
    {
        try
        {
            handle.queue_position_bottom();
        }
        catch (const std::exception &exc)
        {
            qDebug() << Q_FUNC_INFO << " fails: " << exc.what();
        }
    }

    QMap<QString, CategoryOptions> expandCategories(const QMap<QString, CategoryOptions> &categories)
    {
        QMap<QString, CategoryOptions> expanded = categories;

        for (auto i = categories.cbegin(); i != categories.cend(); ++i)
        {
            const QString &category = i.key();
            for (const QString &subcat : asConst(Session::expandCategory(category)))
            {
                if (!expanded.contains(subcat))
                    expanded[subcat] = {};
            }
        }

        return expanded;
    }

    QString toString(const lt::socket_type_t socketType)
    {
        switch (socketType)
        {
#ifdef QBT_USES_LIBTORRENT2
        case lt::socket_type_t::http:
            return u"HTTP"_s;
        case lt::socket_type_t::http_ssl:
            return u"HTTP_SSL"_s;
#endif
        case lt::socket_type_t::i2p:
            return u"I2P"_s;
        case lt::socket_type_t::socks5:
            return u"SOCKS5"_s;
#ifdef QBT_USES_LIBTORRENT2
        case lt::socket_type_t::socks5_ssl:
            return u"SOCKS5_SSL"_s;
#endif
        case lt::socket_type_t::tcp:
            return u"TCP"_s;
        case lt::socket_type_t::tcp_ssl:
            return u"TCP_SSL"_s;
#ifdef QBT_USES_LIBTORRENT2
        case lt::socket_type_t::utp:
            return u"UTP"_s;
#else
        case lt::socket_type_t::udp:
            return u"UDP"_s;
#endif
        case lt::socket_type_t::utp_ssl:
            return u"UTP_SSL"_s;
#if LIBTORRENT_VERSION_NUM >= 20100
        case lt::socket_type_t::rtc:
            return u"WebRTC"_s;
#endif
        }
        return u"INVALID"_s;
    }

    QString toString(const lt::address &address)
    {
        try
        {
            return Utils::String::fromLatin1(address.to_string());
        }
        catch (const std::exception &)
        {
            // suppress conversion error
        }
        return {};
    }

    template <typename T>
    struct LowerLimited
    {
        LowerLimited(T limit, T ret)
            : m_limit(limit)
            , m_ret(ret)
        {
        }

        explicit LowerLimited(T limit)
            : LowerLimited(limit, limit)
        {
        }

        T operator()(T val) const
        {
            return val <= m_limit ? m_ret : val;
        }

    private:
        const T m_limit;
        const T m_ret;
    };

    template <typename T>
    LowerLimited<T> lowerLimited(T limit) { return LowerLimited<T>(limit); }

    template <typename T>
    LowerLimited<T> lowerLimited(T limit, T ret) { return LowerLimited<T>(limit, ret); }

    template <typename T>
    auto clampValue(const T lower, const T upper)
    {
        return [lower, upper](const T value) -> T
        {
            return std::clamp(value, lower, upper);
        };
    }

#ifdef Q_OS_WIN
    QString convertIfaceNameToGuid(const QString &name)
    {
        // Under Windows XP or on Qt version <= 5.5 'name' will be a GUID already.
        const QUuid uuid(name);
        if (!uuid.isNull())
            return uuid.toString().toUpper(); // Libtorrent expects the GUID in uppercase

        const std::wstring nameWStr = name.toStdWString();
        NET_LUID luid {};
        const LONG res = ::ConvertInterfaceNameToLuidW(nameWStr.c_str(), &luid);
        if (res == 0)
        {
            GUID guid;
            if (::ConvertInterfaceLuidToGuid(&luid, &guid) == 0)
                return QUuid(guid).toString().toUpper();
        }

        return {};
    }
#endif

    constexpr lt::move_flags_t toNative(const MoveStorageMode mode)
    {
        switch (mode)
        {
        case MoveStorageMode::FailIfExist:
            return lt::move_flags_t::fail_if_exist;
        case MoveStorageMode::KeepExistingFiles:
            return lt::move_flags_t::dont_replace;
        case MoveStorageMode::Overwrite:
            return lt::move_flags_t::always_replace_files;
        default:
            Q_UNREACHABLE();
            break;
        }
    }

#ifdef QBT_USES_LIBTORRENT2
    template <typename T>
    concept HasInfoHashMember = requires (T t) { { t.info_hashes } -> std::convertible_to<InfoHash>; };

    template <typename T>
    concept HasInfoHashMemberFn = requires (T t) { { t.info_hashes() } -> std::convertible_to<InfoHash>; };

    template <HasInfoHashMember T>
    InfoHash getInfoHash(const T &t) { return t.info_hashes; }

    template <HasInfoHashMemberFn T>
    InfoHash getInfoHash(const T &t) { return t.info_hashes(); }

    InfoHash getInfoHash(const lt::add_torrent_params &addTorrentParams)
    {
        const bool hasMetadata = (addTorrentParams.ti && addTorrentParams.ti->is_valid());
        return hasMetadata ? getInfoHash(*addTorrentParams.ti) : InfoHash(addTorrentParams.info_hashes);
    }
 #else
    template <typename T>
    concept HasInfoHashMember = requires (T t) { { t.info_hash } -> std::convertible_to<InfoHash>; };

    template <typename T>
    concept HasInfoHashMemberFn = requires (T t) { { t.info_hash() } -> std::convertible_to<InfoHash>; };

    template <HasInfoHashMember T>
    InfoHash getInfoHash(const T &t) { return t.info_hash; }

    template <HasInfoHashMemberFn T>
    InfoHash getInfoHash(const T &t) { return t.info_hash(); }

    InfoHash getInfoHash(const lt::add_torrent_params &addTorrentParams)
    {
        const bool hasMetadata = (addTorrentParams.ti && addTorrentParams.ti->is_valid());
        return hasMetadata ? getInfoHash(*addTorrentParams.ti) : InfoHash(addTorrentParams.info_hash);
    }
 #endif
}

struct BitTorrent::SessionImpl::ResumeSessionContext final : public QObject
{
    using QObject::QObject;

    ResumeDataStorage *startupStorage = nullptr;
    ResumeDataStorageType currentStorageType = ResumeDataStorageType::Legacy;
    QList<LoadedResumeData> loadedResumeData;
    int processingResumeDataCount = 0;
    int64_t totalResumeDataCount = 0;
    int64_t finishedResumeDataCount = 0;
    bool isLoadFinished = false;
    bool isLoadedResumeDataHandlingEnqueued = false;
    QSet<QString> recoveredCategories;
#ifdef QBT_USES_LIBTORRENT2
    QSet<TorrentID> indexedTorrents;
    QSet<TorrentID> skippedIDs;
#endif
};

const int addTorrentParamsId = qRegisterMetaType<AddTorrentParams>();

Session *SessionImpl::m_instance = nullptr;

void Session::initInstance()
{
    if (!SessionImpl::m_instance)
        SessionImpl::m_instance = new SessionImpl;
}

void Session::freeInstance()
{
    delete SessionImpl::m_instance;
    SessionImpl::m_instance = nullptr;
}

Session *Session::instance()
{
    return SessionImpl::m_instance;
}

bool Session::isValidCategoryName(const QString &name)
{
    const QRegularExpression re {uR"(^([^\\\/]|[^\\\/]([^\\\/]|\/(?=[^\/]))*[^\\\/])$)"_s};
    return (name.isEmpty() || (name.indexOf(re) == 0));
}

QString Session::subcategoryName(const QString &category)
{
    const qsizetype sepIndex = category.lastIndexOf(u'/');
    if (sepIndex >= 0)
        return category.sliced(sepIndex + 1);

    return category;
}

QString Session::parentCategoryName(const QString &category)
{
    const qsizetype sepIndex = category.lastIndexOf(u'/');
    if (sepIndex >= 0)
        return category.first(sepIndex);

    return {};
}

QStringList Session::expandCategory(const QString &category)
{
    QStringList result;
    int index = 0;
    while ((index = category.indexOf(u'/', index)) >= 0)
    {
        result << category.first(index);
        ++index;
    }
    result << category;

    return result;
}

#define BITTORRENT_KEY(name) u"BitTorrent/" name
#define BITTORRENT_SESSION_KEY(name) BITTORRENT_KEY(u"Session/") name

SessionImpl::SessionImpl(QObject *parent)
    : Session(parent)
    , m_DHTBootstrapNodes(BITTORRENT_SESSION_KEY(u"DHTBootstrapNodes"_s), DEFAULT_DHT_BOOTSTRAP_NODES)
    , m_isDHTEnabled(BITTORRENT_SESSION_KEY(u"DHTEnabled"_s), true)
    , m_isLSDEnabled(BITTORRENT_SESSION_KEY(u"LSDEnabled"_s), true)
    , m_isPeXEnabled(BITTORRENT_SESSION_KEY(u"PeXEnabled"_s), true)
    , m_isIPFilteringEnabled(BITTORRENT_SESSION_KEY(u"IPFilteringEnabled"_s), false)
    , m_isTrackerFilteringEnabled(BITTORRENT_SESSION_KEY(u"TrackerFilteringEnabled"_s), false)
    , m_IPFilterFile(BITTORRENT_SESSION_KEY(u"IPFilter"_s))
    , m_announceToAllTrackers(BITTORRENT_SESSION_KEY(u"AnnounceToAllTrackers"_s), false)
    , m_announceToAllTiers(BITTORRENT_SESSION_KEY(u"AnnounceToAllTiers"_s), true)
    , m_asyncIOThreads(BITTORRENT_SESSION_KEY(u"AsyncIOThreadsCount"_s), 10)
    , m_hashingThreads(BITTORRENT_SESSION_KEY(u"HashingThreadsCount"_s), 1)
    , m_filePoolSize(BITTORRENT_SESSION_KEY(u"FilePoolSize"_s), 100)
    , m_checkingMemUsage(BITTORRENT_SESSION_KEY(u"CheckingMemUsageSize"_s), 32)
    , m_diskCacheSize(BITTORRENT_SESSION_KEY(u"DiskCacheSize"_s), -1)
    , m_diskCacheTTL(BITTORRENT_SESSION_KEY(u"DiskCacheTTL"_s), 60)
    , m_diskQueueSize(BITTORRENT_SESSION_KEY(u"DiskQueueSize"_s), (1024 * 1024))
    , m_diskIOType(BITTORRENT_SESSION_KEY(u"DiskIOType"_s), DiskIOType::Default)
    , m_diskIOReadMode(BITTORRENT_SESSION_KEY(u"DiskIOReadMode"_s), DiskIOReadMode::EnableOSCache)
    , m_diskIOWriteMode(BITTORRENT_SESSION_KEY(u"DiskIOWriteMode"_s), DiskIOWriteMode::EnableOSCache)
#ifdef Q_OS_WIN
    , m_coalesceReadWriteEnabled(BITTORRENT_SESSION_KEY(u"CoalesceReadWrite"_s), true)
#else
    , m_coalesceReadWriteEnabled(BITTORRENT_SESSION_KEY(u"CoalesceReadWrite"_s), false)
#endif
    , m_usePieceExtentAffinity(BITTORRENT_SESSION_KEY(u"PieceExtentAffinity"_s), false)
    , m_isSuggestMode(BITTORRENT_SESSION_KEY(u"SuggestMode"_s), false)
    , m_sendBufferWatermark(BITTORRENT_SESSION_KEY(u"SendBufferWatermark"_s), 500)
    , m_sendBufferLowWatermark(BITTORRENT_SESSION_KEY(u"SendBufferLowWatermark"_s), 10)
    , m_sendBufferWatermarkFactor(BITTORRENT_SESSION_KEY(u"SendBufferWatermarkFactor"_s), 50)
    , m_connectionSpeed(BITTORRENT_SESSION_KEY(u"ConnectionSpeed"_s), 30)
    , m_socketSendBufferSize(BITTORRENT_SESSION_KEY(u"SocketSendBufferSize"_s), 0)
    , m_socketReceiveBufferSize(BITTORRENT_SESSION_KEY(u"SocketReceiveBufferSize"_s), 0)
    , m_socketBacklogSize(BITTORRENT_SESSION_KEY(u"SocketBacklogSize"_s), 30)
    , m_isAnonymousModeEnabled(BITTORRENT_SESSION_KEY(u"AnonymousModeEnabled"_s), false)
    , m_isQueueingEnabled(BITTORRENT_SESSION_KEY(u"QueueingSystemEnabled"_s), false)
    , m_maxActiveDownloads(BITTORRENT_SESSION_KEY(u"MaxActiveDownloads"_s), 3, lowerLimited(-1))
    , m_maxActiveUploads(BITTORRENT_SESSION_KEY(u"MaxActiveUploads"_s), 3, lowerLimited(-1))
    , m_maxActiveTorrents(BITTORRENT_SESSION_KEY(u"MaxActiveTorrents"_s), 5, lowerLimited(-1))
    , m_ignoreSlowTorrentsForQueueing(BITTORRENT_SESSION_KEY(u"IgnoreSlowTorrentsForQueueing"_s), false)
    , m_downloadRateForSlowTorrents(BITTORRENT_SESSION_KEY(u"SlowTorrentsDownloadRate"_s), 2)
    , m_uploadRateForSlowTorrents(BITTORRENT_SESSION_KEY(u"SlowTorrentsUploadRate"_s), 2)
    , m_slowTorrentsInactivityTimer(BITTORRENT_SESSION_KEY(u"SlowTorrentsInactivityTimer"_s), 60)
    , m_outgoingPortsMin(BITTORRENT_SESSION_KEY(u"OutgoingPortsMin"_s), 0)
    , m_outgoingPortsMax(BITTORRENT_SESSION_KEY(u"OutgoingPortsMax"_s), 0)
    , m_UPnPLeaseDuration(BITTORRENT_SESSION_KEY(u"UPnPLeaseDuration"_s), 0)
    , m_peerDSCP(BITTORRENT_SESSION_KEY(u"PeerToS"_s), 0x01)
    , m_ignoreLimitsOnLAN(BITTORRENT_SESSION_KEY(u"IgnoreLimitsOnLAN"_s), false)
    , m_includeOverheadInLimits(BITTORRENT_SESSION_KEY(u"IncludeOverheadInLimits"_s), false)
    , m_announceIP(BITTORRENT_SESSION_KEY(u"AnnounceIP"_s))
    , m_announcePort(BITTORRENT_SESSION_KEY(u"AnnouncePort"_s), 0)
    , m_maxConcurrentHTTPAnnounces(BITTORRENT_SESSION_KEY(u"MaxConcurrentHTTPAnnounces"_s), 50)
    , m_isReannounceWhenAddressChangedEnabled(BITTORRENT_SESSION_KEY(u"ReannounceWhenAddressChanged"_s), false)
    , m_stopTrackerTimeout(BITTORRENT_SESSION_KEY(u"StopTrackerTimeout"_s), 2)
    , m_maxConnections(BITTORRENT_SESSION_KEY(u"MaxConnections"_s), 500, lowerLimited(0, -1))
    , m_maxUploads(BITTORRENT_SESSION_KEY(u"MaxUploads"_s), 20, lowerLimited(0, -1))
    , m_maxConnectionsPerTorrent(BITTORRENT_SESSION_KEY(u"MaxConnectionsPerTorrent"_s), 100, lowerLimited(0, -1))
    , m_maxUploadsPerTorrent(BITTORRENT_SESSION_KEY(u"MaxUploadsPerTorrent"_s), 4, lowerLimited(0, -1))
    , m_btProtocol(BITTORRENT_SESSION_KEY(u"BTProtocol"_s), BTProtocol::Both
        , clampValue(BTProtocol::Both, BTProtocol::UTP))
    , m_isUTPRateLimited(BITTORRENT_SESSION_KEY(u"uTPRateLimited"_s), true)
    , m_utpMixedMode(BITTORRENT_SESSION_KEY(u"uTPMixedMode"_s), MixedModeAlgorithm::TCP
        , clampValue(MixedModeAlgorithm::TCP, MixedModeAlgorithm::Proportional))
    , m_hostnameCacheTTL(BITTORRENT_SESSION_KEY(u"HostnameCacheTTL"_s), 1200)
    , m_IDNSupportEnabled(BITTORRENT_SESSION_KEY(u"IDNSupportEnabled"_s), false)
    , m_multiConnectionsPerIpEnabled(BITTORRENT_SESSION_KEY(u"MultiConnectionsPerIp"_s), false)
    , m_validateHTTPSTrackerCertificate(BITTORRENT_SESSION_KEY(u"ValidateHTTPSTrackerCertificate"_s), true)
    , m_SSRFMitigationEnabled(BITTORRENT_SESSION_KEY(u"SSRFMitigation"_s), true)
    , m_blockPeersOnPrivilegedPorts(BITTORRENT_SESSION_KEY(u"BlockPeersOnPrivilegedPorts"_s), false)
    , m_isAddTrackersEnabled(BITTORRENT_SESSION_KEY(u"AddTrackersEnabled"_s), false)
    , m_additionalTrackers(BITTORRENT_SESSION_KEY(u"AdditionalTrackers"_s))
    , m_isAddTrackersFromURLEnabled(BITTORRENT_SESSION_KEY(u"AddTrackersFromURLEnabled"_s), false)
    , m_additionalTrackersURL(BITTORRENT_SESSION_KEY(u"AdditionalTrackersURL"_s))
    , m_globalMaxRatio(BITTORRENT_SESSION_KEY(u"GlobalMaxRatio"_s), -1, [](qreal r) { return r < 0 ? -1. : r; })
    , m_globalMaxSeedingMinutes(BITTORRENT_SESSION_KEY(u"GlobalMaxSeedingMinutes"_s)
        , NO_SEEDING_TIME_LIMIT, lowerLimited(NO_SEEDING_TIME_LIMIT))
    , m_globalMaxInactiveSeedingMinutes(BITTORRENT_SESSION_KEY(u"GlobalMaxInactiveSeedingMinutes"_s)
        , NO_SEEDING_TIME_LIMIT, lowerLimited(NO_SEEDING_TIME_LIMIT))
    , m_isAddTorrentToQueueTop(BITTORRENT_SESSION_KEY(u"AddTorrentToTopOfQueue"_s), false)
    , m_isAddTorrentStopped(BITTORRENT_SESSION_KEY(u"AddTorrentStopped"_s), false)
    , m_torrentStopCondition(BITTORRENT_SESSION_KEY(u"TorrentStopCondition"_s), Torrent::StopCondition::None)
    , m_torrentContentLayout(BITTORRENT_SESSION_KEY(u"TorrentContentLayout"_s), TorrentContentLayout::Original)
    , m_isAppendExtensionEnabled(BITTORRENT_SESSION_KEY(u"AddExtensionToIncompleteFiles"_s), false)
    , m_isUnwantedFolderEnabled(BITTORRENT_SESSION_KEY(u"UseUnwantedFolder"_s), false)
    , m_refreshInterval(BITTORRENT_SESSION_KEY(u"RefreshInterval"_s), 1500)
    , m_isPreallocationEnabled(BITTORRENT_SESSION_KEY(u"Preallocation"_s), false)
    , m_torrentExportDirectory(BITTORRENT_SESSION_KEY(u"TorrentExportDirectory"_s))
    , m_finishedTorrentExportDirectory(BITTORRENT_SESSION_KEY(u"FinishedTorrentExportDirectory"_s))
    , m_globalDownloadSpeedLimit(BITTORRENT_SESSION_KEY(u"GlobalDLSpeedLimit"_s), 0, lowerLimited(0))
    , m_globalUploadSpeedLimit(BITTORRENT_SESSION_KEY(u"GlobalUPSpeedLimit"_s), 0, lowerLimited(0))
    , m_altGlobalDownloadSpeedLimit(BITTORRENT_SESSION_KEY(u"AlternativeGlobalDLSpeedLimit"_s), 10, lowerLimited(0))
    , m_altGlobalUploadSpeedLimit(BITTORRENT_SESSION_KEY(u"AlternativeGlobalUPSpeedLimit"_s), 10, lowerLimited(0))
    , m_isAltGlobalSpeedLimitEnabled(BITTORRENT_SESSION_KEY(u"UseAlternativeGlobalSpeedLimit"_s), false)
    , m_isBandwidthSchedulerEnabled(BITTORRENT_SESSION_KEY(u"BandwidthSchedulerEnabled"_s), false)
    , m_isPerformanceWarningEnabled(BITTORRENT_SESSION_KEY(u"PerformanceWarning"_s), false)
    , m_saveResumeDataInterval(BITTORRENT_SESSION_KEY(u"SaveResumeDataInterval"_s), 60)
    , m_saveStatisticsInterval(BITTORRENT_SESSION_KEY(u"SaveStatisticsInterval"_s), 15)
    , m_shutdownTimeout(BITTORRENT_SESSION_KEY(u"ShutdownTimeout"_s), -1)
    , m_port(BITTORRENT_SESSION_KEY(u"Port"_s), -1)
    , m_sslEnabled(BITTORRENT_SESSION_KEY(u"SSL/Enabled"_s), false)
    , m_sslPort(BITTORRENT_SESSION_KEY(u"SSL/Port"_s), -1)
    , m_networkInterface(BITTORRENT_SESSION_KEY(u"Interface"_s))
    , m_networkInterfaceName(BITTORRENT_SESSION_KEY(u"InterfaceName"_s))
    , m_networkInterfaceAddress(BITTORRENT_SESSION_KEY(u"InterfaceAddress"_s))
    , m_encryption(BITTORRENT_SESSION_KEY(u"Encryption"_s), 0)
    , m_maxActiveCheckingTorrents(BITTORRENT_SESSION_KEY(u"MaxActiveCheckingTorrents"_s), 1)
    , m_isProxyPeerConnectionsEnabled(BITTORRENT_SESSION_KEY(u"ProxyPeerConnections"_s), false)
    , m_chokingAlgorithm(BITTORRENT_SESSION_KEY(u"ChokingAlgorithm"_s), ChokingAlgorithm::FixedSlots
        , clampValue(ChokingAlgorithm::FixedSlots, ChokingAlgorithm::RateBased))
    , m_seedChokingAlgorithm(BITTORRENT_SESSION_KEY(u"SeedChokingAlgorithm"_s), SeedChokingAlgorithm::FastestUpload
        , clampValue(SeedChokingAlgorithm::RoundRobin, SeedChokingAlgorithm::AntiLeech))
    , m_storedTags(BITTORRENT_SESSION_KEY(u"Tags"_s))
    , m_shareLimitAction(BITTORRENT_SESSION_KEY(u"ShareLimitAction"_s), ShareLimitAction::Stop
        , [](const ShareLimitAction action) { return (action == ShareLimitAction::Default) ? ShareLimitAction::Stop : action; })
    , m_savePath(BITTORRENT_SESSION_KEY(u"DefaultSavePath"_s), specialFolderLocation(SpecialFolder::Downloads))
    , m_downloadPath(BITTORRENT_SESSION_KEY(u"TempPath"_s), (savePath() / Path(u"temp"_s)))
    , m_isDownloadPathEnabled(BITTORRENT_SESSION_KEY(u"TempPathEnabled"_s), false)
    , m_useCategoryPathsInManualMode(BITTORRENT_SESSION_KEY(u"UseCategoryPathsInManualMode"_s), false)
    , m_isAutoTMMDisabledByDefault(BITTORRENT_SESSION_KEY(u"DisableAutoTMMByDefault"_s), true)
    , m_isDisableAutoTMMWhenCategoryChanged(BITTORRENT_SESSION_KEY(u"DisableAutoTMMTriggers/CategoryChanged"_s), false)
    , m_isDisableAutoTMMWhenDefaultSavePathChanged(BITTORRENT_SESSION_KEY(u"DisableAutoTMMTriggers/DefaultSavePathChanged"_s), true)
    , m_isDisableAutoTMMWhenCategorySavePathChanged(BITTORRENT_SESSION_KEY(u"DisableAutoTMMTriggers/CategorySavePathChanged"_s), true)
    , m_isTrackerEnabled(BITTORRENT_KEY(u"TrackerEnabled"_s), false)
    , m_peerTurnover(BITTORRENT_SESSION_KEY(u"PeerTurnover"_s), 4)
    , m_peerTurnoverCutoff(BITTORRENT_SESSION_KEY(u"PeerTurnoverCutOff"_s), 90)
    , m_peerTurnoverInterval(BITTORRENT_SESSION_KEY(u"PeerTurnoverInterval"_s), 300)
    , m_requestQueueSize(BITTORRENT_SESSION_KEY(u"RequestQueueSize"_s), 500)
    , m_isExcludedFileNamesEnabled(BITTORRENT_KEY(u"ExcludedFileNamesEnabled"_s), false)
    , m_excludedFileNames(BITTORRENT_SESSION_KEY(u"ExcludedFileNames"_s))
    , m_bannedIPs(u"State/BannedIPs"_s, QStringList(), Algorithm::sorted<QStringList>)
    , m_resumeDataStorageType(BITTORRENT_SESSION_KEY(u"ResumeDataStorageType"_s), ResumeDataStorageType::Legacy)
    , m_isMergeTrackersEnabled(BITTORRENT_KEY(u"MergeTrackersEnabled"_s), false)
    , m_isI2PEnabled {BITTORRENT_SESSION_KEY(u"I2P/Enabled"_s), false}
    , m_I2PAddress {BITTORRENT_SESSION_KEY(u"I2P/Address"_s), u"127.0.0.1"_s}
    , m_I2PPort {BITTORRENT_SESSION_KEY(u"I2P/Port"_s), 7656}
    , m_I2PMixedMode {BITTORRENT_SESSION_KEY(u"I2P/MixedMode"_s), false}
    , m_I2PInboundQuantity {BITTORRENT_SESSION_KEY(u"I2P/InboundQuantity"_s), 3}
    , m_I2POutboundQuantity {BITTORRENT_SESSION_KEY(u"I2P/OutboundQuantity"_s), 3}
    , m_I2PInboundLength {BITTORRENT_SESSION_KEY(u"I2P/InboundLength"_s), 3}
    , m_I2POutboundLength {BITTORRENT_SESSION_KEY(u"I2P/OutboundLength"_s), 3}
    , m_torrentContentRemoveOption {BITTORRENT_SESSION_KEY(u"TorrentContentRemoveOption"_s), TorrentContentRemoveOption::Delete}
    , m_startPaused {BITTORRENT_SESSION_KEY(u"StartPaused"_s)}
    , m_autoBanUnknownPeer(BITTORRENT_SESSION_KEY(u"AutoBanUnknownPeer"_s), false)
    , m_autoBanBTPlayerPeer(BITTORRENT_SESSION_KEY(u"AutoBanBTPlayerPeer"_s), false)
    , m_shadowBan(BITTORRENT_SESSION_KEY(u"ShadowBan"_s), false)
    , m_shadowBannedIPs(u"State/ShadowBannedIPs"_s, QStringList(), Algorithm::sorted<QStringList>)
    , m_seedingLimitTimer {new QTimer(this)}
    , m_resumeDataTimer {new QTimer(this)}
    , m_ioThread {new QThread}
    , m_asyncWorker {new QThreadPool(this)}
    , m_recentErroredTorrentsTimer {new QTimer(this)}
    , m_freeDiskSpaceChecker {new FreeDiskSpaceChecker(savePath())}
    , m_freeDiskSpaceCheckingTimer {new QTimer(this)}
{
    // It is required to perform async access to libtorrent sequentially
    m_asyncWorker->setMaxThreadCount(1);
    m_asyncWorker->setObjectName("SessionImpl m_asyncWorker");

    m_alerts.reserve(1024);

    if (port() < 0)
        m_port = Utils::Random::rand(1024, 65535);
    if (sslPort() < 0)
    {
        m_sslPort = Utils::Random::rand(1024, 65535);
        while (m_sslPort == port())
            m_sslPort = Utils::Random::rand(1024, 65535);
    }

    m_recentErroredTorrentsTimer->setSingleShot(true);
    m_recentErroredTorrentsTimer->setInterval(1s);
    connect(m_recentErroredTorrentsTimer, &QTimer::timeout
        , this, [this]() { m_recentErroredTorrents.clear(); });

    m_seedingLimitTimer->setInterval(10s);
    connect(m_seedingLimitTimer, &QTimer::timeout, this, [this]
    {
        // We shouldn't iterate over `m_torrents` in the loop below
        // since `deleteTorrent()` modifies it indirectly
        const QHash<TorrentID, TorrentImpl *> torrents {m_torrents};
        for (TorrentImpl *torrent : torrents)
            processTorrentShareLimits(torrent);
    });

    initializeNativeSession();
    configureComponents();

    if (isBandwidthSchedulerEnabled())
        enableBandwidthScheduler();

    loadCategories();

    const QStringList storedTags = m_storedTags.get();
    for (const QString &tagStr : storedTags)
    {
        if (const Tag tag {tagStr}; tag.isValid())
            m_tags.insert(tag);
    }

    updateSeedingLimitTimer();
    populateAdditionalTrackers();
    if (isExcludedFileNamesEnabled())
        populateExcludedFileNamesRegExpList();

    connect(Net::ProxyConfigurationManager::instance()
        , &Net::ProxyConfigurationManager::proxyConfigurationChanged
        , this, &SessionImpl::configureDeferred);

    m_freeDiskSpaceChecker->moveToThread(m_ioThread.get());
    connect(m_ioThread.get(), &QThread::finished, m_freeDiskSpaceChecker, &QObject::deleteLater);
    m_freeDiskSpaceCheckingTimer->setInterval(FREEDISKSPACE_CHECK_TIMEOUT);
    m_freeDiskSpaceCheckingTimer->setSingleShot(true);
    connect(m_freeDiskSpaceCheckingTimer, &QTimer::timeout, m_freeDiskSpaceChecker, &FreeDiskSpaceChecker::check);
    connect(m_freeDiskSpaceChecker, &FreeDiskSpaceChecker::checked, this, [this](const qint64 value)
    {
        m_freeDiskSpace = value;
        m_freeDiskSpaceCheckingTimer->start();
        emit freeDiskSpaceChecked(m_freeDiskSpace);
    });

    m_fileSearcher = new FileSearcher;
    m_fileSearcher->moveToThread(m_ioThread.get());
    connect(m_ioThread.get(), &QThread::finished, m_fileSearcher, &QObject::deleteLater);

    m_torrentContentRemover = new TorrentContentRemover;
    m_torrentContentRemover->moveToThread(m_ioThread.get());
    connect(m_ioThread.get(), &QThread::finished, m_torrentContentRemover, &QObject::deleteLater);
    connect(m_torrentContentRemover, &TorrentContentRemover::jobFinished, this, &SessionImpl::torrentContentRemovingFinished);

    m_ioThread->setObjectName("SessionImpl m_ioThread");
    m_ioThread->start();

    QMetaObject::invokeMethod(m_freeDiskSpaceChecker, &FreeDiskSpaceChecker::check);

    initMetrics();
    loadStatistics();

    // initialize PortForwarder instance
    new PortForwarderImpl(this);

    // start embedded tracker
    enableTracker(isTrackerEnabled());

    prepareStartup();

    // Migrate customize trackers url list setting from Enhanced
    const auto *pref = Preferences::instance();
    const bool migrated = pref->getMigrateStatus();
    if (!m_isAddTrackersFromURLEnabled && !migrated) {
        Preferences::instance()->setMigrateStatus(!migrated);

        const bool isEnabled = pref->isAutoUpdateTrackersEnabled();
        const QString trackersListUrl = pref->getTrackersListUrl();
        m_isAddTrackersFromURLEnabled = isEnabled;
        m_additionalTrackersURL = trackersListUrl;
    }

    m_updateTrackersFromURLTimer = new QTimer(this);
    m_updateTrackersFromURLTimer->setInterval(24h);
    connect(m_updateTrackersFromURLTimer, &QTimer::timeout, this, &SessionImpl::updateTrackersFromURL);

    if (isAddTrackersFromURLEnabled())
    {
        updateTrackersFromFile();
        updateTrackersFromURL();
        m_updateTrackersFromURLTimer->start();
    }
}

SessionImpl::~SessionImpl()
{
    m_nativeSession->pause();

    const auto timeout = (m_shutdownTimeout >= 0) ? (static_cast<qint64>(m_shutdownTimeout) * 1000) : -1;
    const QDeadlineTimer shutdownDeadlineTimer {timeout};

    if (m_torrentsQueueChanged)
    {
        m_nativeSession->post_torrent_updates();
        m_torrentsQueueChanged = false;
        m_needSaveTorrentsQueue = true;
    }

    // Do some bittorrent related saving
    // After this, (ideally) no more important alerts will be generated/handled
    saveResumeData();

    saveStatistics();

    // We must delete FilterParserThread
    // before we delete lt::session
    delete m_filterParser;

    // We must delete PortForwarderImpl before
    // we delete lt::session
    delete Net::PortForwarder::instance();

    // We must stop "async worker" only after deletion
    // of all the components that could potentially use it
    m_asyncWorker->clear();
    m_asyncWorker->waitForDone();

    auto *nativeSessionProxy = new lt::session_proxy(m_nativeSession->abort());
    delete m_nativeSession;

    qDebug("Deleting resume data storage...");
    delete m_resumeDataStorage;
    LogMsg(tr("Saving resume data completed."));

    auto *sessionTerminateThread = QThread::create([nativeSessionProxy]()
    {
        qDebug("Deleting libtorrent session...");
        delete nativeSessionProxy;
    });
    sessionTerminateThread->setObjectName("~SessionImpl sessionTerminateThread");
    connect(sessionTerminateThread, &QThread::finished, sessionTerminateThread, &QObject::deleteLater);
    sessionTerminateThread->start();
    if (sessionTerminateThread->wait(shutdownDeadlineTimer))
        LogMsg(tr("BitTorrent session successfully finished."));
    else
        LogMsg(tr("Session shutdown timed out."));
}

QString SessionImpl::getDHTBootstrapNodes() const
{
    const QString nodes = m_DHTBootstrapNodes;
    return !nodes.isEmpty() ? nodes : DEFAULT_DHT_BOOTSTRAP_NODES;
}

void SessionImpl::setDHTBootstrapNodes(const QString &nodes)
{
    if (nodes == m_DHTBootstrapNodes)
        return;

    m_DHTBootstrapNodes = nodes;
    configureDeferred();
}

bool SessionImpl::isDHTEnabled() const
{
    return m_isDHTEnabled;
}

void SessionImpl::setDHTEnabled(bool enabled)
{
    if (enabled != m_isDHTEnabled)
    {
        m_isDHTEnabled = enabled;
        configureDeferred();
        LogMsg(tr("Distributed Hash Table (DHT) support: %1").arg(enabled ? tr("ON") : tr("OFF")), Log::INFO);
    }
}

bool SessionImpl::isLSDEnabled() const
{
    return m_isLSDEnabled;
}

void SessionImpl::setLSDEnabled(const bool enabled)
{
    if (enabled != m_isLSDEnabled)
    {
        m_isLSDEnabled = enabled;
        configureDeferred();
        LogMsg(tr("Local Peer Discovery support: %1").arg(enabled ? tr("ON") : tr("OFF"))
            , Log::INFO);
    }
}

bool SessionImpl::isPeXEnabled() const
{
    return m_isPeXEnabled;
}

void SessionImpl::setPeXEnabled(const bool enabled)
{
    m_isPeXEnabled = enabled;
    if (m_wasPexEnabled != enabled)
        LogMsg(tr("Restart is required to toggle Peer Exchange (PeX) support"), Log::WARNING);
}

bool SessionImpl::isDownloadPathEnabled() const
{
    return m_isDownloadPathEnabled;
}

void SessionImpl::setDownloadPathEnabled(const bool enabled)
{
    if (enabled != isDownloadPathEnabled())
    {
        m_isDownloadPathEnabled = enabled;
        for (TorrentImpl *const torrent : asConst(m_torrents))
            torrent->handleCategoryOptionsChanged();
    }
}

bool SessionImpl::isAppendExtensionEnabled() const
{
    return m_isAppendExtensionEnabled;
}

void SessionImpl::setAppendExtensionEnabled(const bool enabled)
{
    if (isAppendExtensionEnabled() != enabled)
    {
        m_isAppendExtensionEnabled = enabled;

        // append or remove .!qB extension for incomplete files
        for (TorrentImpl *const torrent : asConst(m_torrents))
            torrent->handleAppendExtensionToggled();
    }
}

bool SessionImpl::isUnwantedFolderEnabled() const
{
    return m_isUnwantedFolderEnabled;
}

void SessionImpl::setUnwantedFolderEnabled(const bool enabled)
{
    if (isUnwantedFolderEnabled() != enabled)
    {
        m_isUnwantedFolderEnabled = enabled;

        // append or remove .!qB extension for incomplete files
        for (TorrentImpl *const torrent : asConst(m_torrents))
            torrent->handleUnwantedFolderToggled();
    }
}

int SessionImpl::refreshInterval() const
{
    return m_refreshInterval;
}

void SessionImpl::setRefreshInterval(const int value)
{
    if (value != refreshInterval())
    {
        m_refreshInterval = value;
    }
}

bool SessionImpl::isPreallocationEnabled() const
{
    return m_isPreallocationEnabled;
}

void SessionImpl::setPreallocationEnabled(const bool enabled)
{
    m_isPreallocationEnabled = enabled;
}

Path SessionImpl::torrentExportDirectory() const
{
    return m_torrentExportDirectory;
}

void SessionImpl::setTorrentExportDirectory(const Path &path)
{
    if (path != torrentExportDirectory())
        m_torrentExportDirectory = path;
}

Path SessionImpl::finishedTorrentExportDirectory() const
{
    return m_finishedTorrentExportDirectory;
}

void SessionImpl::setFinishedTorrentExportDirectory(const Path &path)
{
    if (path != finishedTorrentExportDirectory())
        m_finishedTorrentExportDirectory = path;
}

Path SessionImpl::savePath() const
{
    // TODO: Make sure it is always non-empty
    return m_savePath;
}

Path SessionImpl::downloadPath() const
{
    // TODO: Make sure it is always non-empty
    return m_downloadPath;
}

QStringList SessionImpl::categories() const
{
    return m_categories.keys();
}

CategoryOptions SessionImpl::categoryOptions(const QString &categoryName) const
{
    return m_categories.value(categoryName);
}

Path SessionImpl::categorySavePath(const QString &categoryName) const
{
    return categorySavePath(categoryName, categoryOptions(categoryName));
}

Path SessionImpl::categorySavePath(const QString &categoryName, const CategoryOptions &options) const
{
    Path basePath = savePath();
    if (categoryName.isEmpty())
        return basePath;

    Path path = options.savePath;
    if (path.isEmpty())
    {
        // use implicit save path
        path = Utils::Fs::toValidPath(subcategoryName(categoryName));
        basePath = categorySavePath(parentCategoryName(categoryName));
    }

    return (path.isAbsolute() ? path : (basePath / path));
}

Path SessionImpl::categoryDownloadPath(const QString &categoryName) const
{
    return categoryDownloadPath(categoryName, categoryOptions(categoryName));
}

Path SessionImpl::categoryDownloadPath(const QString &categoryName, const CategoryOptions &options) const
{
    const DownloadPathOption downloadPathOption = resolveCategoryDownloadPathOption(categoryName, options.downloadPath);
    if (!downloadPathOption.enabled)
        return {};

    if (categoryName.isEmpty())
        return downloadPath();

    const QString name = subcategoryName(categoryName);
    const Path path = !downloadPathOption.path.isEmpty()
            ? downloadPathOption.path
            : Utils::Fs::toValidPath(name); // use implicit download path

    if (path.isAbsolute())
        return path;

    const QString parentName = parentCategoryName(categoryName);
    CategoryOptions parentOptions = categoryOptions(parentName);
    // Even if download path of parent category is disabled (directly or by inheritance)
    // we need to construct the one as if it would be enabled.
    if (!parentOptions.downloadPath || !parentOptions.downloadPath->enabled)
        parentOptions.downloadPath = {true, {}};
    const Path parentDownloadPath = categoryDownloadPath(parentName, parentOptions);
    const Path basePath = parentDownloadPath.isEmpty() ? downloadPath() : parentDownloadPath;
    return (basePath / path);
}

qreal SessionImpl::categoryRatioLimit(const QString &categoryName) const
{
    if (categoryName.isEmpty())
        return globalMaxRatio();

    if (const auto ratioLimit = categoryOptions(categoryName).ratioLimit;
            ratioLimit != DEFAULT_RATIO_LIMIT)
    {
        return ratioLimit;
    }

    return categoryRatioLimit(parentCategoryName(categoryName));
}

int SessionImpl::categorySeedingTimeLimit(const QString &categoryName) const
{
    if (categoryName.isEmpty())
        return globalMaxSeedingMinutes();

    if (const auto seedingTimeLimit = categoryOptions(categoryName).seedingTimeLimit;
            seedingTimeLimit != DEFAULT_SEEDING_TIME_LIMIT)
    {
        return seedingTimeLimit;
    }

    return categorySeedingTimeLimit(parentCategoryName(categoryName));
}

int SessionImpl::categoryInactiveSeedingTimeLimit(const QString &categoryName) const
{
    if (categoryName.isEmpty())
        return globalMaxInactiveSeedingMinutes();

    if (const auto inactiveSeedingTimeLimit = categoryOptions(categoryName).inactiveSeedingTimeLimit;
            inactiveSeedingTimeLimit != DEFAULT_SEEDING_TIME_LIMIT)
    {
        return inactiveSeedingTimeLimit;
    }

    return categoryInactiveSeedingTimeLimit(parentCategoryName(categoryName));
}

ShareLimitAction SessionImpl::categoryShareLimitAction(const QString &categoryName) const
{
    if (categoryName.isEmpty())
        return shareLimitAction();

    if (const auto shareLimitAction = categoryOptions(categoryName).shareLimitAction;
            shareLimitAction != ShareLimitAction::Default)
    {
        return shareLimitAction;
    }

    return categoryShareLimitAction(parentCategoryName(categoryName));
}

DownloadPathOption SessionImpl::resolveCategoryDownloadPathOption(const QString &categoryName, const std::optional<DownloadPathOption> &option) const
{
    if (categoryName.isEmpty())
        return {isDownloadPathEnabled(), Path()};

    if (option.has_value())
        return *option;

    const QString parentName = parentCategoryName(categoryName);
    return resolveCategoryDownloadPathOption(parentName, categoryOptions(parentName).downloadPath);
}

bool SessionImpl::addCategory(const QString &name, const CategoryOptions &options)
{
    if (name.isEmpty())
        return false;

    if (!isValidCategoryName(name) || m_categories.contains(name))
        return false;

    for (const QString &parent : asConst(expandCategory(name)))
    {
        if ((parent != name) && !m_categories.contains(parent))
        {
            m_categories[parent] = {};
            emit categoryAdded(parent);
        }
    }

    m_categories[name] = options;
    storeCategories();
    emit categoryAdded(name);

    return true;
}

bool SessionImpl::setCategoryOptions(const QString &categoryName, const CategoryOptions &options)
{
    const auto it = m_categories.find(categoryName);
    if (it == m_categories.end())
        return false;

    CategoryOptions &currentOptions = it.value();
    if (options == currentOptions)
        return true;

    if (isDisableAutoTMMWhenCategorySavePathChanged()
            && ((options.savePath != currentOptions.savePath) || (options.downloadPath != currentOptions.downloadPath)))
    {
        // This should be done before changing the category options
        // to prevent the torrent from being moved at the new save path.

        for (TorrentImpl *const torrent : asConst(m_torrents))
        {
            if (torrent->category() == categoryName)
                torrent->setAutoTMMEnabled(false);
        }
    }

    currentOptions = options;
    storeCategories();

    for (TorrentImpl *const torrent : asConst(m_torrents))
    {
        if (torrent->category() == categoryName)
            torrent->handleCategoryOptionsChanged();
    }

    emit categoryOptionsChanged(categoryName);
    return true;
}

bool SessionImpl::removeCategory(const QString &name)
{
    const QString parentCategory = parentCategoryName(name);
    for (TorrentImpl *const torrent : asConst(m_torrents))
    {
        if (torrent->belongsToCategory(name))
            torrent->setCategory(parentCategory);
    }

    // remove stored category and its subcategories if exist
    bool result = false;
    // remove subcategories
    const QString test = name + u'/';
    Algorithm::removeIf(m_categories, [this, &test, &result](const QString &category, const CategoryOptions &)
    {
        if (category.startsWith(test))
        {
            result = true;
            emit categoryRemoved(category);
            return true;
        }
        return false;
    });

    result = (m_categories.remove(name) > 0) || result;

    if (result)
    {
        // update stored categories
        storeCategories();
        emit categoryRemoved(name);
    }

    return result;
}

bool SessionImpl::useCategoryPathsInManualMode() const
{
    return m_useCategoryPathsInManualMode;
}

void SessionImpl::setUseCategoryPathsInManualMode(const bool value)
{
    m_useCategoryPathsInManualMode = value;
}

Path SessionImpl::suggestedSavePath(const QString &categoryName, std::optional<bool> useAutoTMM) const
{
    const bool useCategoryPaths = useAutoTMM.value_or(!isAutoTMMDisabledByDefault()) || useCategoryPathsInManualMode();
    const auto path = (useCategoryPaths ? categorySavePath(categoryName) : savePath());
    return path;
}

Path SessionImpl::suggestedDownloadPath(const QString &categoryName, std::optional<bool> useAutoTMM) const
{
    const bool useCategoryPaths = useAutoTMM.value_or(!isAutoTMMDisabledByDefault()) || useCategoryPathsInManualMode();
    const auto categoryDownloadPath = this->categoryDownloadPath(categoryName);
    const auto path = ((useCategoryPaths && !categoryDownloadPath.isEmpty()) ? categoryDownloadPath : downloadPath());
    return path;
}

TagSet SessionImpl::tags() const
{
    return m_tags;
}

bool SessionImpl::hasTag(const Tag &tag) const
{
    return m_tags.contains(tag);
}

bool SessionImpl::addTag(const Tag &tag)
{
    if (!tag.isValid() || hasTag(tag))
        return false;

    m_tags.insert(tag);
    m_storedTags = QStringList(m_tags.cbegin(), m_tags.cend());

    emit tagAdded(tag);
    return true;
}

bool SessionImpl::removeTag(const Tag &tag)
{
    if (m_tags.remove(tag))
    {
        for (TorrentImpl *const torrent : asConst(m_torrents))
            torrent->removeTag(tag);

        m_storedTags = QStringList(m_tags.cbegin(), m_tags.cend());

        emit tagRemoved(tag);
        return true;
    }
    return false;
}

bool SessionImpl::isAutoTMMDisabledByDefault() const
{
    return m_isAutoTMMDisabledByDefault;
}

void SessionImpl::setAutoTMMDisabledByDefault(const bool value)
{
    m_isAutoTMMDisabledByDefault = value;
}

bool SessionImpl::isDisableAutoTMMWhenCategoryChanged() const
{
    return m_isDisableAutoTMMWhenCategoryChanged;
}

void SessionImpl::setDisableAutoTMMWhenCategoryChanged(const bool value)
{
    m_isDisableAutoTMMWhenCategoryChanged = value;
}

bool SessionImpl::isDisableAutoTMMWhenDefaultSavePathChanged() const
{
    return m_isDisableAutoTMMWhenDefaultSavePathChanged;
}

void SessionImpl::setDisableAutoTMMWhenDefaultSavePathChanged(const bool value)
{
    m_isDisableAutoTMMWhenDefaultSavePathChanged = value;
}

bool SessionImpl::isDisableAutoTMMWhenCategorySavePathChanged() const
{
    return m_isDisableAutoTMMWhenCategorySavePathChanged;
}

void SessionImpl::setDisableAutoTMMWhenCategorySavePathChanged(const bool value)
{
    m_isDisableAutoTMMWhenCategorySavePathChanged = value;
}

bool SessionImpl::isAddTorrentToQueueTop() const
{
    return m_isAddTorrentToQueueTop;
}

void SessionImpl::setAddTorrentToQueueTop(bool value)
{
    m_isAddTorrentToQueueTop = value;
}

bool SessionImpl::isAddTorrentStopped() const
{
    return m_isAddTorrentStopped;
}

void SessionImpl::setAddTorrentStopped(const bool value)
{
    m_isAddTorrentStopped = value;
}

Torrent::StopCondition SessionImpl::torrentStopCondition() const
{
    return m_torrentStopCondition;
}

void SessionImpl::setTorrentStopCondition(const Torrent::StopCondition stopCondition)
{
    m_torrentStopCondition = stopCondition;
}

bool SessionImpl::isTrackerEnabled() const
{
    return m_isTrackerEnabled;
}

void SessionImpl::setTrackerEnabled(const bool enabled)
{
    if (m_isTrackerEnabled != enabled)
        m_isTrackerEnabled = enabled;

    // call enableTracker() unconditionally, otherwise port change won't trigger
    // tracker restart
    enableTracker(enabled);
}

qreal SessionImpl::globalMaxRatio() const
{
    return m_globalMaxRatio;
}

// Torrents with a ratio superior to the given value will
// be automatically deleted
void SessionImpl::setGlobalMaxRatio(qreal ratio)
{
    if (ratio < 0)
        ratio = NO_RATIO_LIMIT;

    if (ratio != globalMaxRatio())
    {
        m_globalMaxRatio = ratio;
        updateSeedingLimitTimer();
    }
}

int SessionImpl::globalMaxSeedingMinutes() const
{
    return m_globalMaxSeedingMinutes;
}

void SessionImpl::setGlobalMaxSeedingMinutes(int minutes)
{
    minutes = std::max(minutes, NO_SEEDING_TIME_LIMIT);

    if (minutes != globalMaxSeedingMinutes())
    {
        m_globalMaxSeedingMinutes = minutes;
        updateSeedingLimitTimer();
    }
}

int SessionImpl::globalMaxInactiveSeedingMinutes() const
{
    return m_globalMaxInactiveSeedingMinutes;
}

void SessionImpl::setGlobalMaxInactiveSeedingMinutes(int minutes)
{
    minutes = std::max(minutes, NO_SEEDING_TIME_LIMIT);

    if (minutes != globalMaxInactiveSeedingMinutes())
    {
        m_globalMaxInactiveSeedingMinutes = minutes;
        updateSeedingLimitTimer();
    }
}

void SessionImpl::applyBandwidthLimits()
{
    lt::settings_pack settingsPack;
    settingsPack.set_int(lt::settings_pack::download_rate_limit, downloadSpeedLimit());
    settingsPack.set_int(lt::settings_pack::upload_rate_limit, uploadSpeedLimit());
    m_nativeSession->apply_settings(std::move(settingsPack));
}

void SessionImpl::configure()
{
    const bool isListenInterfaceChanged = !m_listenInterfaceConfigured;

    m_nativeSession->apply_settings(loadLTSettings());
    configureComponents();

    if (isListenInterfaceChanged && isReannounceWhenAddressChangedEnabled())
        reannounceToAllTrackers();

    m_deferredConfigureScheduled = false;
}

void SessionImpl::configureComponents()
{
    // This function contains components/actions that:
    // 1. Need to be setup at start up
    // 2. When deferred configure is called

    configurePeerClasses();

    if (!m_IPFilteringConfigured)
    {
        if (isIPFilteringEnabled())
            enableIPFilter();
        else
            disableIPFilter();
        m_IPFilteringConfigured = true;
    }
}

void SessionImpl::prepareStartup()
{
    qDebug("Initializing torrents resume data storage...");

    const Path dbPath = specialFolderLocation(SpecialFolder::Data) / Path(u"torrents.db"_s);
    const bool dbStorageExists = dbPath.exists();

    auto *context = new ResumeSessionContext(this);
    context->currentStorageType = resumeDataStorageType();

    if (context->currentStorageType == ResumeDataStorageType::SQLite)
    {
        m_resumeDataStorage = new DBResumeDataStorage(dbPath, this);

        if (!dbStorageExists)
        {
            const Path dataPath = specialFolderLocation(SpecialFolder::Data) / Path(u"BT_backup"_s);
            context->startupStorage = new BencodeResumeDataStorage(dataPath, this);
        }
    }
    else
    {
        const Path dataPath = specialFolderLocation(SpecialFolder::Data) / Path(u"BT_backup"_s);
        m_resumeDataStorage = new BencodeResumeDataStorage(dataPath, this);

        if (dbStorageExists)
            context->startupStorage = new DBResumeDataStorage(dbPath, this);
    }

    if (!context->startupStorage)
        context->startupStorage = m_resumeDataStorage;

    connect(context->startupStorage, &ResumeDataStorage::loadStarted, context
            , [this, context](const QList<TorrentID> &torrents)
    {
        context->totalResumeDataCount = torrents.size();
#ifdef QBT_USES_LIBTORRENT2
        context->indexedTorrents = QSet<TorrentID>(torrents.cbegin(), torrents.cend());
#endif

        handleLoadedResumeData(context);
    });

    connect(context->startupStorage, &ResumeDataStorage::loadFinished, context, [context]()
    {
        context->isLoadFinished = true;
    });

    connect(this, &SessionImpl::addTorrentAlertsReceived, context, [this, context](const qsizetype alertsCount)
    {
        context->processingResumeDataCount -= alertsCount;
        context->finishedResumeDataCount += alertsCount;
        if (!context->isLoadedResumeDataHandlingEnqueued)
        {
            QMetaObject::invokeMethod(this, [this, context] { handleLoadedResumeData(context); }, Qt::QueuedConnection);
            context->isLoadedResumeDataHandlingEnqueued = true;
        }

        if (!m_refreshEnqueued)
        {
            m_nativeSession->post_torrent_updates();
            m_refreshEnqueued = true;
        }

        emit startupProgressUpdated((context->finishedResumeDataCount * 100.) / context->totalResumeDataCount);
    });

    context->startupStorage->loadAll();
}

void SessionImpl::handleLoadedResumeData(ResumeSessionContext *context)
{
    context->isLoadedResumeDataHandlingEnqueued = false;

    int count = context->processingResumeDataCount;
    while (context->processingResumeDataCount < MAX_PROCESSING_RESUMEDATA_COUNT)
    {
        if (context->loadedResumeData.isEmpty())
            context->loadedResumeData = context->startupStorage->fetchLoadedResumeData();

        if (context->loadedResumeData.isEmpty())
        {
            if (context->processingResumeDataCount == 0)
            {
                if (context->isLoadFinished)
                {
                    endStartup(context);
                }
                else if (!context->isLoadedResumeDataHandlingEnqueued)
                {
                    QMetaObject::invokeMethod(this, [this, context]() { handleLoadedResumeData(context); }, Qt::QueuedConnection);
                    context->isLoadedResumeDataHandlingEnqueued = true;
                }
            }

            break;
        }

        processNextResumeData(context);
        ++count;
    }

    context->finishedResumeDataCount += (count - context->processingResumeDataCount);
}

void SessionImpl::processNextResumeData(ResumeSessionContext *context)
{
    auto [torrentID, loadResumeDataResult] = context->loadedResumeData.takeFirst();

#ifdef QBT_USES_LIBTORRENT2
    if (context->skippedIDs.contains(torrentID))
        return;
#endif

    if (!loadResumeDataResult)
    {
        LogMsg(tr("Failed to resume torrent. Torrent: \"%1\". Reason: \"%2\"")
               .arg(torrentID.toString(), loadResumeDataResult.error()), Log::CRITICAL);
        return;
    }

    LoadTorrentParams resumeData = std::move(*loadResumeDataResult);
    bool needStore = false;

    const InfoHash infoHash = getInfoHash(resumeData.ltAddTorrentParams);
#ifdef QBT_USES_LIBTORRENT2
    const bool isHybrid = infoHash.isHybrid();
    const auto torrentIDv2 = TorrentID::fromInfoHash(infoHash);
    const auto torrentIDv1 = TorrentID::fromSHA1Hash(infoHash.v1());
    if (torrentID == torrentIDv2)
    {
        if (isHybrid && context->indexedTorrents.contains(torrentIDv1))
        {
            // if we don't have metadata, try to find it in alternative "resume data"
            if (!resumeData.ltAddTorrentParams.ti)
            {
                const nonstd::expected<LoadTorrentParams, QString> loadAltResumeDataResult = context->startupStorage->load(torrentIDv1);
                if (loadAltResumeDataResult)
                    resumeData.ltAddTorrentParams.ti = loadAltResumeDataResult->ltAddTorrentParams.ti;
            }

            // remove alternative "resume data" and skip the attempt to load it
            m_resumeDataStorage->remove(torrentIDv1);
            context->skippedIDs.insert(torrentIDv1);
        }
    }
    else if (torrentID == torrentIDv1)
    {
        torrentID = torrentIDv2;
        needStore = true;
        m_resumeDataStorage->remove(torrentIDv1);

        if (context->indexedTorrents.contains(torrentID))
        {
            context->skippedIDs.insert(torrentID);

            if (nonstd::expected<LoadTorrentParams, QString> loadPreferredResumeDataResult = context->startupStorage->load(torrentID))
            {
                std::shared_ptr<lt::torrent_info> ti = resumeData.ltAddTorrentParams.ti;
                resumeData = std::move(*loadPreferredResumeDataResult);
                if (!resumeData.ltAddTorrentParams.ti)
                    resumeData.ltAddTorrentParams.ti = std::move(ti);
            }
        }
    }
    else
    {
        LogMsg(tr("Failed to resume torrent: inconsistent torrent ID is detected. Torrent: \"%1\"")
               .arg(torrentID.toString()), Log::WARNING);
        return;
    }
#else
    if (torrentID != TorrentID::fromInfoHash(infoHash))
    {
        LogMsg(tr("Failed to resume torrent: inconsistent torrent ID is detected. Torrent: \"%1\"")
               .arg(torrentID.toString()), Log::WARNING);
        return;
    }
#endif

    if (m_resumeDataStorage != context->startupStorage)
       needStore = true;

    // TODO: Remove the following upgrade code in v4.6
    // == BEGIN UPGRADE CODE ==
    if (!needStore)
    {
        if (m_needUpgradeDownloadPath && isDownloadPathEnabled() && !resumeData.useAutoTMM)
        {
            resumeData.downloadPath = downloadPath();
            needStore = true;
        }
    }
    // == END UPGRADE CODE ==

    if (needStore)
        m_resumeDataStorage->store(torrentID, resumeData);

    const QString category = resumeData.category;
    bool isCategoryRecovered = context->recoveredCategories.contains(category);
    if (!category.isEmpty() && (isCategoryRecovered || !m_categories.contains(category)))
    {
        if (!isCategoryRecovered)
        {
            if (addCategory(category))
            {
                context->recoveredCategories.insert(category);
                isCategoryRecovered = true;
                LogMsg(tr("Detected inconsistent data: category is missing from the configuration file."
                          " Category will be recovered but its settings will be reset to default."
                          " Torrent: \"%1\". Category: \"%2\"").arg(torrentID.toString(), category), Log::WARNING);
            }
            else
            {
                resumeData.category.clear();
                LogMsg(tr("Detected inconsistent data: invalid category. Torrent: \"%1\". Category: \"%2\"")
                       .arg(torrentID.toString(), category), Log::WARNING);
            }
        }

        // We should check isCategoryRecovered again since the category
        // can be just recovered by the code above
        if (isCategoryRecovered && resumeData.useAutoTMM)
        {
            const Path storageLocation {resumeData.ltAddTorrentParams.save_path};
            if ((storageLocation != categorySavePath(resumeData.category)) && (storageLocation != categoryDownloadPath(resumeData.category)))
            {
                resumeData.useAutoTMM = false;
                resumeData.savePath = storageLocation;
                resumeData.downloadPath = {};
                LogMsg(tr("Detected mismatch between the save paths of the recovered category and the current save path of the torrent."
                          " Torrent is now switched to Manual mode."
                          " Torrent: \"%1\". Category: \"%2\"").arg(torrentID.toString(), category), Log::WARNING);
            }
        }
    }

    std::erase_if(resumeData.tags, [this, &torrentID](const Tag &tag)
    {
        if (hasTag(tag))
            return false;

        if (addTag(tag))
        {
            LogMsg(tr("Detected inconsistent data: tag is missing from the configuration file."
                      " Tag will be recovered."
                      " Torrent: \"%1\". Tag: \"%2\"").arg(torrentID.toString(), tag.toString()), Log::WARNING);
            return false;
        }

        LogMsg(tr("Detected inconsistent data: invalid tag. Torrent: \"%1\". Tag: \"%2\"")
               .arg(torrentID.toString(), tag.toString()), Log::WARNING);
        return true;
    });

    resumeData.ltAddTorrentParams.userdata = LTClientData(new ExtensionData);
#ifndef QBT_USES_LIBTORRENT2
    resumeData.ltAddTorrentParams.storage = customStorageConstructor;
#endif

    qDebug() << "Starting up torrent" << torrentID.toString() << "...";
    m_nativeSession->async_add_torrent(resumeData.ltAddTorrentParams);
    m_addTorrentAlertHandlers.append([this, resumeData = std::move(resumeData)](const lt::add_torrent_alert *alert) mutable
    {
        if (alert->error)
        {
            const QString msg = QString::fromStdString(alert->message());
            LogMsg(tr("Failed to load torrent. Reason: \"%1\"").arg(msg), Log::WARNING);
        }
        else
        {
            Torrent *torrent = createTorrent(alert->handle, std::move(resumeData));
            m_loadedTorrents.append(torrent);

            LogMsg(tr("Restored torrent. Torrent: \"%1\"").arg(torrent->name()));
        }
    });

    ++context->processingResumeDataCount;
}

void SessionImpl::endStartup(ResumeSessionContext *context)
{
    if (m_resumeDataStorage != context->startupStorage)
    {
        if (isQueueingSystemEnabled())
            saveTorrentsQueue();

        const Path dbPath = context->startupStorage->path();
        context->startupStorage->deleteLater();

        if (context->currentStorageType == ResumeDataStorageType::Legacy)
        {
            connect(context->startupStorage, &QObject::destroyed, this, [dbPath]
            {
                Utils::Fs::removeFile(dbPath);
            });
        }
    }

    context->deleteLater();
    connect(context, &QObject::destroyed, this, [this]
    {
        if (!m_isPaused)
            m_nativeSession->resume();

        if (m_refreshEnqueued)
            m_refreshEnqueued = false;
        else
            enqueueRefresh();

        m_statisticsLastUpdateTimer.start();

        // Regular saving of fastresume data
        connect(m_resumeDataTimer, &QTimer::timeout, this, &SessionImpl::generateResumeData);
        const int saveInterval = saveResumeDataInterval();
        if (saveInterval > 0)
        {
            m_resumeDataTimer->setInterval(std::chrono::minutes(saveInterval));
            m_resumeDataTimer->start();
        }

        auto wakeupCheckTimer = new QTimer(this);
        connect(wakeupCheckTimer, &QTimer::timeout, this, [this]
        {
            const bool hasSystemSlept = m_wakeupCheckTimestamp.durationElapsed() > 100s;
            if (hasSystemSlept)
            {
                LogMsg(tr("System wake-up event detected. Re-announcing to all the trackers..."));
                reannounceToAllTrackers();
            }

            m_wakeupCheckTimestamp.start();
        });
        m_wakeupCheckTimestamp.start();
        wakeupCheckTimer->start(30s);

        m_isRestored = true;
        emit startupProgressUpdated(100);
        emit restored();
    });
}

void SessionImpl::initializeNativeSession()
{
    lt::settings_pack pack = loadLTSettings();

    const std::string peerId = lt::generate_fingerprint(PEER_ID, QBT_VERSION_MAJOR, QBT_VERSION_MINOR, QBT_VERSION_BUGFIX, QBT_VERSION_BUILD);
    pack.set_str(lt::settings_pack::peer_fingerprint, peerId);

    pack.set_bool(lt::settings_pack::listen_system_port_fallback, false);
    pack.set_str(lt::settings_pack::user_agent, USER_AGENT.toStdString());
    pack.set_bool(lt::settings_pack::use_dht_as_fallback, false);
    // Speed up exit
    pack.set_int(lt::settings_pack::auto_scrape_interval, 1200); // 20 minutes
    pack.set_int(lt::settings_pack::auto_scrape_min_interval, 900); // 15 minutes
    // libtorrent 1.1 enables UPnP & NAT-PMP by default
    // turn them off before `lt::session` ctor to avoid split second effects
    pack.set_bool(lt::settings_pack::enable_upnp, false);
    pack.set_bool(lt::settings_pack::enable_natpmp, false);

#ifdef QBT_USES_LIBTORRENT2
    // preserve the same behavior as in earlier libtorrent versions
    pack.set_bool(lt::settings_pack::enable_set_file_valid_data, true);

    // This is a special case. We use MMap disk IO but tweak it to always fallback to pread/pwrite.
    if (diskIOType() == DiskIOType::SimplePreadPwrite)
    {
        pack.set_int(lt::settings_pack::mmap_file_size_cutoff, std::numeric_limits<int>::max());
        pack.set_int(lt::settings_pack::disk_write_mode, lt::settings_pack::mmap_write_mode_t::always_pwrite);
    }
#endif

    lt::session_params sessionParams {std::move(pack), {}};
#ifdef QBT_USES_LIBTORRENT2
    switch (diskIOType())
    {
    case DiskIOType::Posix:
        sessionParams.disk_io_constructor = customPosixDiskIOConstructor;
        break;
    case DiskIOType::MMap:
    case DiskIOType::SimplePreadPwrite:
        sessionParams.disk_io_constructor = customMMapDiskIOConstructor;
        break;
#if LIBTORRENT_VERSION_NUM >= 20100
    case DiskIOType::PreadPwrite:
        sessionParams.disk_io_constructor = customPreadDiskIOConstructor;
#endif
    default:
        sessionParams.disk_io_constructor = customDiskIOConstructor;
        break;
    }
#endif

#if LIBTORRENT_VERSION_NUM < 20100
    m_nativeSession = new lt::session(sessionParams, lt::session::paused);
#else
    m_nativeSession = new lt::session(sessionParams);
    m_nativeSession->pause();
#endif

    LogMsg(tr("Peer ID: \"%1\"").arg(QString::fromStdString(peerId)), Log::INFO);
    LogMsg(tr("HTTP User-Agent: \"%1\"").arg(USER_AGENT), Log::INFO);
    LogMsg(tr("Distributed Hash Table (DHT) support: %1").arg(isDHTEnabled() ? tr("ON") : tr("OFF")), Log::INFO);
    LogMsg(tr("Local Peer Discovery support: %1").arg(isLSDEnabled() ? tr("ON") : tr("OFF")), Log::INFO);
    // Enhanced features
    m_nativeSession->add_extension(&create_drop_bad_peers_plugin);
    if (isAutoBanUnknownPeerEnabled()) {
        m_nativeSession->add_extension(&create_drop_unknown_peers_plugin);
        m_nativeSession->add_extension(&create_drop_offline_downloader_plugin);
    }
    if (isAutoBanBTPlayerPeerEnabled())
        m_nativeSession->add_extension(&create_drop_bittorrent_media_player_plugin);
    m_nativeSession->add_extension(std::make_shared<peer_filter_session_plugin>());
    if (isShadowBanEnabled())
        m_nativeSession->add_extension(&create_peer_shadowban_plugin);

    LogMsg(tr("Peer Exchange (PeX) support: %1").arg(isPeXEnabled() ? tr("ON") : tr("OFF")), Log::INFO);
    LogMsg(tr("Anonymous mode: %1").arg(isAnonymousModeEnabled() ? tr("ON") : tr("OFF")), Log::INFO);
    LogMsg(tr("Encryption support: %1").arg((encryption() == 0) ? tr("ON") : ((encryption() == 1) ? tr("FORCED") : tr("OFF"))), Log::INFO);

    m_nativeSession->set_alert_notify([this]()
    {
        QMetaObject::invokeMethod(this, &SessionImpl::readAlerts, Qt::QueuedConnection);
    });

    // Enabling plugins
    m_nativeSession->add_extension(&lt::create_smart_ban_plugin);
    m_nativeSession->add_extension(&lt::create_ut_metadata_plugin);
    if (isPeXEnabled())
        m_nativeSession->add_extension(&lt::create_ut_pex_plugin);

    auto nativeSessionExtension = std::make_shared<NativeSessionExtension>();
    m_nativeSession->add_extension(nativeSessionExtension);
    m_nativeSessionExtension = nativeSessionExtension.get();
}

void SessionImpl::processBannedIPs(lt::ip_filter &filter)
{
    // First, import current filter
    for (const QString &ip : asConst(m_bannedIPs.get()))
    {
        lt::error_code ec;
        const lt::address addr = lt::make_address(ip.toLatin1().constData(), ec);
        Q_ASSERT(!ec);
        if (!ec)
            filter.add_rule(addr, addr, lt::ip_filter::blocked);
    }
}

void SessionImpl::initMetrics()
{
    const auto findMetricIndex = [](const char *name) -> int
    {
        const int index = lt::find_metric_idx(name);
        Q_ASSERT(index >= 0);
        return index;
    };

    m_metricIndices =
    {
        .net =
        {
            .hasIncomingConnections = findMetricIndex("net.has_incoming_connections"),
            .sentPayloadBytes = findMetricIndex("net.sent_payload_bytes"),
            .recvPayloadBytes = findMetricIndex("net.recv_payload_bytes"),
            .sentBytes = findMetricIndex("net.sent_bytes"),
            .recvBytes = findMetricIndex("net.recv_bytes"),
            .sentIPOverheadBytes = findMetricIndex("net.sent_ip_overhead_bytes"),
            .recvIPOverheadBytes = findMetricIndex("net.recv_ip_overhead_bytes"),
            .sentTrackerBytes = findMetricIndex("net.sent_tracker_bytes"),
            .recvTrackerBytes = findMetricIndex("net.recv_tracker_bytes"),
            .recvRedundantBytes = findMetricIndex("net.recv_redundant_bytes"),
            .recvFailedBytes = findMetricIndex("net.recv_failed_bytes")
        },
        .peer =
        {
            .numPeersConnected = findMetricIndex("peer.num_peers_connected"),
            .numPeersUpDisk = findMetricIndex("peer.num_peers_up_disk"),
            .numPeersDownDisk = findMetricIndex("peer.num_peers_down_disk")
        },
        .dht =
        {
            .dhtBytesIn = findMetricIndex("dht.dht_bytes_in"),
            .dhtBytesOut = findMetricIndex("dht.dht_bytes_out"),
            .dhtNodes = findMetricIndex("dht.dht_nodes")
        },
        .disk =
        {
            .diskBlocksInUse = findMetricIndex("disk.disk_blocks_in_use"),
            .numBlocksRead = findMetricIndex("disk.num_blocks_read"),
#ifndef QBT_USES_LIBTORRENT2
            .numBlocksCacheHits = findMetricIndex("disk.num_blocks_cache_hits"),
#endif
            .writeJobs = findMetricIndex("disk.num_write_ops"),
            .readJobs = findMetricIndex("disk.num_read_ops"),
            .hashJobs = findMetricIndex("disk.num_blocks_hashed"),
            .queuedDiskJobs = findMetricIndex("disk.queued_disk_jobs"),
            .diskJobTime = findMetricIndex("disk.disk_job_time")
        }
    };
}

lt::settings_pack SessionImpl::loadLTSettings() const
{
    lt::settings_pack settingsPack;

    const lt::alert_category_t alertMask = lt::alert::error_notification
        | lt::alert::file_progress_notification
        | lt::alert::ip_block_notification
        | lt::alert::peer_notification
        | (isPerformanceWarningEnabled() ? lt::alert::performance_warning : lt::alert_category_t())
        | lt::alert::port_mapping_notification
        | lt::alert::status_notification
        | lt::alert::storage_notification
        | lt::alert::tracker_notification;
    settingsPack.set_int(lt::settings_pack::alert_mask, alertMask);

    settingsPack.set_int(lt::settings_pack::connection_speed, connectionSpeed());

    // from libtorrent doc:
    // It will not take affect until the listen_interfaces settings is updated
    settingsPack.set_int(lt::settings_pack::send_socket_buffer_size, socketSendBufferSize());
    settingsPack.set_int(lt::settings_pack::recv_socket_buffer_size, socketReceiveBufferSize());
    settingsPack.set_int(lt::settings_pack::listen_queue_size, socketBacklogSize());

    applyNetworkInterfacesSettings(settingsPack);

    settingsPack.set_int(lt::settings_pack::download_rate_limit, downloadSpeedLimit());
    settingsPack.set_int(lt::settings_pack::upload_rate_limit, uploadSpeedLimit());

    // The most secure, rc4 only so that all streams are encrypted
    settingsPack.set_int(lt::settings_pack::allowed_enc_level, lt::settings_pack::pe_rc4);
    settingsPack.set_bool(lt::settings_pack::prefer_rc4, true);
    switch (encryption())
    {
    case 0: // Enabled
        settingsPack.set_int(lt::settings_pack::out_enc_policy, lt::settings_pack::pe_enabled);
        settingsPack.set_int(lt::settings_pack::in_enc_policy, lt::settings_pack::pe_enabled);
        break;
    case 1: // Forced
        settingsPack.set_int(lt::settings_pack::out_enc_policy, lt::settings_pack::pe_forced);
        settingsPack.set_int(lt::settings_pack::in_enc_policy, lt::settings_pack::pe_forced);
        break;
    default: // Disabled
        settingsPack.set_int(lt::settings_pack::out_enc_policy, lt::settings_pack::pe_disabled);
        settingsPack.set_int(lt::settings_pack::in_enc_policy, lt::settings_pack::pe_disabled);
    }

    settingsPack.set_int(lt::settings_pack::active_checking, maxActiveCheckingTorrents());

    // I2P
#if defined(QBT_USES_LIBTORRENT2) && TORRENT_USE_I2P
    if (isI2PEnabled())
    {
        settingsPack.set_str(lt::settings_pack::i2p_hostname, I2PAddress().toStdString());
        settingsPack.set_int(lt::settings_pack::i2p_port, I2PPort());
        settingsPack.set_bool(lt::settings_pack::allow_i2p_mixed, I2PMixedMode());
    }
    else
    {
        settingsPack.set_str(lt::settings_pack::i2p_hostname, "");
        settingsPack.set_int(lt::settings_pack::i2p_port, 0);
        settingsPack.set_bool(lt::settings_pack::allow_i2p_mixed, false);
    }

    // I2P session options
    settingsPack.set_int(lt::settings_pack::i2p_inbound_quantity, I2PInboundQuantity());
    settingsPack.set_int(lt::settings_pack::i2p_outbound_quantity, I2POutboundQuantity());
    settingsPack.set_int(lt::settings_pack::i2p_inbound_length, I2PInboundLength());
    settingsPack.set_int(lt::settings_pack::i2p_outbound_length, I2POutboundLength());
#endif

    // proxy
    settingsPack.set_int(lt::settings_pack::proxy_type, lt::settings_pack::none);
    const auto *proxyManager = Net::ProxyConfigurationManager::instance();
    const Net::ProxyConfiguration proxyConfig = proxyManager->proxyConfiguration();
    if ((proxyConfig.type != Net::ProxyType::None) && Preferences::instance()->useProxyForBT())
    {
        switch (proxyConfig.type)
        {
        case Net::ProxyType::SOCKS4:
            settingsPack.set_int(lt::settings_pack::proxy_type, lt::settings_pack::socks4);
            break;

        case Net::ProxyType::HTTP:
            if (proxyConfig.authEnabled)
                settingsPack.set_int(lt::settings_pack::proxy_type, lt::settings_pack::http_pw);
            else
                settingsPack.set_int(lt::settings_pack::proxy_type, lt::settings_pack::http);
            break;

        case Net::ProxyType::SOCKS5:
            if (proxyConfig.authEnabled)
                settingsPack.set_int(lt::settings_pack::proxy_type, lt::settings_pack::socks5_pw);
            else
                settingsPack.set_int(lt::settings_pack::proxy_type, lt::settings_pack::socks5);
            break;

        default:
            break;
        }

        settingsPack.set_str(lt::settings_pack::proxy_hostname, proxyConfig.ip.toStdString());
        settingsPack.set_int(lt::settings_pack::proxy_port, proxyConfig.port);

        if (proxyConfig.authEnabled)
        {
            settingsPack.set_str(lt::settings_pack::proxy_username, proxyConfig.username.toStdString());
            settingsPack.set_str(lt::settings_pack::proxy_password, proxyConfig.password.toStdString());
        }

        settingsPack.set_bool(lt::settings_pack::proxy_peer_connections, isProxyPeerConnectionsEnabled());
        settingsPack.set_bool(lt::settings_pack::proxy_hostnames, proxyConfig.hostnameLookupEnabled);
    }

    settingsPack.set_bool(lt::settings_pack::announce_to_all_trackers, announceToAllTrackers());
    settingsPack.set_bool(lt::settings_pack::announce_to_all_tiers, announceToAllTiers());

    settingsPack.set_int(lt::settings_pack::peer_turnover, peerTurnover());
    settingsPack.set_int(lt::settings_pack::peer_turnover_cutoff, peerTurnoverCutoff());
    settingsPack.set_int(lt::settings_pack::peer_turnover_interval, peerTurnoverInterval());

    settingsPack.set_int(lt::settings_pack::max_out_request_queue, requestQueueSize());

#ifdef QBT_USES_LIBTORRENT2
    settingsPack.set_int(lt::settings_pack::metadata_token_limit, Preferences::instance()->getBdecodeTokenLimit());
#endif

    settingsPack.set_int(lt::settings_pack::aio_threads, asyncIOThreads());
#ifdef QBT_USES_LIBTORRENT2
    settingsPack.set_int(lt::settings_pack::hashing_threads, hashingThreads());
#endif
    settingsPack.set_int(lt::settings_pack::file_pool_size, filePoolSize());

    const int checkingMemUsageSize = checkingMemUsage() * 64;
    settingsPack.set_int(lt::settings_pack::checking_mem_usage, checkingMemUsageSize);

#ifndef QBT_USES_LIBTORRENT2
    const int cacheSize = (diskCacheSize() > -1) ? (diskCacheSize() * 64) : -1;
    settingsPack.set_int(lt::settings_pack::cache_size, cacheSize);
    settingsPack.set_int(lt::settings_pack::cache_expiry, diskCacheTTL());
#endif

    settingsPack.set_int(lt::settings_pack::max_queued_disk_bytes, diskQueueSize());

    switch (diskIOReadMode())
    {
    case DiskIOReadMode::DisableOSCache:
        settingsPack.set_int(lt::settings_pack::disk_io_read_mode, lt::settings_pack::disable_os_cache);
        break;
    case DiskIOReadMode::EnableOSCache:
    default:
        settingsPack.set_int(lt::settings_pack::disk_io_read_mode, lt::settings_pack::enable_os_cache);
        break;
    }

    switch (diskIOWriteMode())
    {
    case DiskIOWriteMode::DisableOSCache:
        settingsPack.set_int(lt::settings_pack::disk_io_write_mode, lt::settings_pack::disable_os_cache);
        break;
    case DiskIOWriteMode::EnableOSCache:
    default:
        settingsPack.set_int(lt::settings_pack::disk_io_write_mode, lt::settings_pack::enable_os_cache);
        break;
#ifdef QBT_USES_LIBTORRENT2
    case DiskIOWriteMode::WriteThrough:
        settingsPack.set_int(lt::settings_pack::disk_io_write_mode, lt::settings_pack::write_through);
        break;
#endif
    }

#ifndef QBT_USES_LIBTORRENT2
    settingsPack.set_bool(lt::settings_pack::coalesce_reads, isCoalesceReadWriteEnabled());
    settingsPack.set_bool(lt::settings_pack::coalesce_writes, isCoalesceReadWriteEnabled());
#endif

    settingsPack.set_bool(lt::settings_pack::piece_extent_affinity, usePieceExtentAffinity());

    settingsPack.set_int(lt::settings_pack::suggest_mode, isSuggestModeEnabled()
                         ? lt::settings_pack::suggest_read_cache : lt::settings_pack::no_piece_suggestions);

    settingsPack.set_int(lt::settings_pack::send_buffer_watermark, sendBufferWatermark() * 1024);
    settingsPack.set_int(lt::settings_pack::send_buffer_low_watermark, sendBufferLowWatermark() * 1024);
    settingsPack.set_int(lt::settings_pack::send_buffer_watermark_factor, sendBufferWatermarkFactor());

    settingsPack.set_bool(lt::settings_pack::anonymous_mode, isAnonymousModeEnabled());

    // Queueing System
    if (isQueueingSystemEnabled())
    {
        settingsPack.set_int(lt::settings_pack::active_downloads, maxActiveDownloads());
        settingsPack.set_int(lt::settings_pack::active_limit, maxActiveTorrents());
        settingsPack.set_int(lt::settings_pack::active_seeds, maxActiveUploads());
        settingsPack.set_bool(lt::settings_pack::dont_count_slow_torrents, ignoreSlowTorrentsForQueueing());
        settingsPack.set_int(lt::settings_pack::inactive_down_rate, downloadRateForSlowTorrents() * 1024); // KiB to Bytes
        settingsPack.set_int(lt::settings_pack::inactive_up_rate, uploadRateForSlowTorrents() * 1024); // KiB to Bytes
        settingsPack.set_int(lt::settings_pack::auto_manage_startup, slowTorrentsInactivityTimer());
    }
    else
    {
        settingsPack.set_int(lt::settings_pack::active_downloads, -1);
        settingsPack.set_int(lt::settings_pack::active_seeds, -1);
        settingsPack.set_int(lt::settings_pack::active_limit, -1);
    }
    settingsPack.set_int(lt::settings_pack::active_tracker_limit, -1);
    settingsPack.set_int(lt::settings_pack::active_dht_limit, -1);
    settingsPack.set_int(lt::settings_pack::active_lsd_limit, -1);
    settingsPack.set_int(lt::settings_pack::alert_queue_size, std::numeric_limits<int>::max() / 2);

    // Outgoing ports
    settingsPack.set_int(lt::settings_pack::outgoing_port, outgoingPortsMin());
    settingsPack.set_int(lt::settings_pack::num_outgoing_ports, (outgoingPortsMax() - outgoingPortsMin()));
    // UPnP lease duration
    settingsPack.set_int(lt::settings_pack::upnp_lease_duration, UPnPLeaseDuration());
    // Type of service
    settingsPack.set_int(lt::settings_pack::peer_dscp, peerDSCP());
    // Include overhead in transfer limits
    settingsPack.set_bool(lt::settings_pack::rate_limit_ip_overhead, includeOverheadInLimits());
    // IP address to announce to trackers
    settingsPack.set_str(lt::settings_pack::announce_ip, announceIP().toStdString());
#if LIBTORRENT_VERSION_NUM >= 20011
    // Port to announce to trackers
    settingsPack.set_int(lt::settings_pack::announce_port, announcePort());
#endif
    // Max concurrent HTTP announces
    settingsPack.set_int(lt::settings_pack::max_concurrent_http_announces, maxConcurrentHTTPAnnounces());
    // Stop tracker timeout
    settingsPack.set_int(lt::settings_pack::stop_tracker_timeout, stopTrackerTimeout());
    // * Max connections limit
    settingsPack.set_int(lt::settings_pack::connections_limit, maxConnections());
    // * Global max upload slots
    settingsPack.set_int(lt::settings_pack::unchoke_slots_limit, maxUploads());
    // uTP
    switch (btProtocol())
    {
    case BTProtocol::Both:
    default:
        settingsPack.set_bool(lt::settings_pack::enable_incoming_tcp, true);
        settingsPack.set_bool(lt::settings_pack::enable_outgoing_tcp, true);
        settingsPack.set_bool(lt::settings_pack::enable_incoming_utp, true);
        settingsPack.set_bool(lt::settings_pack::enable_outgoing_utp, true);
        break;

    case BTProtocol::TCP:
        settingsPack.set_bool(lt::settings_pack::enable_incoming_tcp, true);
        settingsPack.set_bool(lt::settings_pack::enable_outgoing_tcp, true);
        settingsPack.set_bool(lt::settings_pack::enable_incoming_utp, false);
        settingsPack.set_bool(lt::settings_pack::enable_outgoing_utp, false);
        break;

    case BTProtocol::UTP:
        settingsPack.set_bool(lt::settings_pack::enable_incoming_tcp, false);
        settingsPack.set_bool(lt::settings_pack::enable_outgoing_tcp, false);
        settingsPack.set_bool(lt::settings_pack::enable_incoming_utp, true);
        settingsPack.set_bool(lt::settings_pack::enable_outgoing_utp, true);
        break;
    }

    switch (utpMixedMode())
    {
    case MixedModeAlgorithm::TCP:
    default:
        settingsPack.set_int(lt::settings_pack::mixed_mode_algorithm, lt::settings_pack::prefer_tcp);
        break;
    case MixedModeAlgorithm::Proportional:
        settingsPack.set_int(lt::settings_pack::mixed_mode_algorithm, lt::settings_pack::peer_proportional);
        break;
    }

    settingsPack.set_int(lt::settings_pack::resolver_cache_timeout, hostnameCacheTTL());

    settingsPack.set_bool(lt::settings_pack::allow_idna, isIDNSupportEnabled());

    settingsPack.set_bool(lt::settings_pack::allow_multiple_connections_per_ip, multiConnectionsPerIpEnabled());

    settingsPack.set_bool(lt::settings_pack::validate_https_trackers, validateHTTPSTrackerCertificate());

    settingsPack.set_bool(lt::settings_pack::ssrf_mitigation, isSSRFMitigationEnabled());

    settingsPack.set_bool(lt::settings_pack::no_connect_privileged_ports, blockPeersOnPrivilegedPorts());

    settingsPack.set_bool(lt::settings_pack::apply_ip_filter_to_trackers, isTrackerFilteringEnabled());

    settingsPack.set_str(lt::settings_pack::dht_bootstrap_nodes, getDHTBootstrapNodes().toStdString());
    settingsPack.set_bool(lt::settings_pack::enable_dht, isDHTEnabled());
    settingsPack.set_bool(lt::settings_pack::enable_lsd, isLSDEnabled());

    switch (chokingAlgorithm())
    {
    case ChokingAlgorithm::FixedSlots:
    default:
        settingsPack.set_int(lt::settings_pack::choking_algorithm, lt::settings_pack::fixed_slots_choker);
        break;
    case ChokingAlgorithm::RateBased:
        settingsPack.set_int(lt::settings_pack::choking_algorithm, lt::settings_pack::rate_based_choker);
        break;
    }

    switch (seedChokingAlgorithm())
    {
    case SeedChokingAlgorithm::RoundRobin:
        settingsPack.set_int(lt::settings_pack::seed_choking_algorithm, lt::settings_pack::round_robin);
        break;
    case SeedChokingAlgorithm::FastestUpload:
    default:
        settingsPack.set_int(lt::settings_pack::seed_choking_algorithm, lt::settings_pack::fastest_upload);
        break;
    case SeedChokingAlgorithm::AntiLeech:
        settingsPack.set_int(lt::settings_pack::seed_choking_algorithm, lt::settings_pack::anti_leech);
        break;
    }

    return settingsPack;
}

void SessionImpl::applyNetworkInterfacesSettings(lt::settings_pack &settingsPack) const
{
    if (m_listenInterfaceConfigured)
        return;

    if (port() > 0)  // user has specified port number
        settingsPack.set_int(lt::settings_pack::max_retry_port_bind, 0);

    QStringList endpoints;
    QStringList outgoingInterfaces;
    QStringList portStrings = {u':' + QString::number(port())};
    if (isSSLEnabled())
        portStrings.append(u':' + QString::number(sslPort()) + u's');

    for (const QString &ip : asConst(getListeningIPs()))
    {
        const QHostAddress addr {ip};
        if (!addr.isNull())
        {
            const bool isIPv6 = (addr.protocol() == QAbstractSocket::IPv6Protocol);
            const QString ip = isIPv6
                          ? Utils::Net::canonicalIPv6Addr(addr).toString()
                          : addr.toString();

            for (const QString &portString : asConst(portStrings))
                endpoints << ((isIPv6 ? (u'[' + ip + u']') : ip) + portString);

            if ((ip != u"0.0.0.0") && (ip != u"::"))
                outgoingInterfaces << ip;
        }
        else
        {
            // ip holds an interface name
#ifdef Q_OS_WIN
            // On Vista+ versions and after Qt 5.5 QNetworkInterface::name() returns
            // the interface's LUID and not the GUID.
            // Libtorrent expects GUIDs for the 'listen_interfaces' setting.
            const QString guid = convertIfaceNameToGuid(ip);
            if (!guid.isEmpty())
            {
                for (const QString &portString : asConst(portStrings))
                    endpoints << (guid + portString);
                outgoingInterfaces << guid;
            }
            else
            {
                LogMsg(tr("Could not find GUID of network interface. Interface: \"%1\"").arg(ip), Log::WARNING);
                // Since we can't get the GUID, we'll pass the interface name instead.
                // Otherwise an empty string will be passed to outgoing_interface which will cause IP leak.
                for (const QString &portString : asConst(portStrings))
                    endpoints << (ip + portString);
                outgoingInterfaces << ip;
            }
#else
            for (const QString &portString : asConst(portStrings))
                endpoints << (ip + portString);
            outgoingInterfaces << ip;
#endif
        }
    }

    const QString finalEndpoints = endpoints.join(u',');
    settingsPack.set_str(lt::settings_pack::listen_interfaces, finalEndpoints.toStdString());
    LogMsg(tr("Trying to listen on the following list of IP addresses: \"%1\"").arg(finalEndpoints));

    settingsPack.set_str(lt::settings_pack::outgoing_interfaces, outgoingInterfaces.join(u',').toStdString());
    m_listenInterfaceConfigured = true;
}

void SessionImpl::configurePeerClasses()
{
    lt::ip_filter f;
    // lt::make_address("255.255.255.255") crashes on some people's systems
    // so instead we use address_v4::broadcast()
    // Proactively do the same for 0.0.0.0 and address_v4::any()
    f.add_rule(lt::address_v4::any()
               , lt::address_v4::broadcast()
               , 1 << LT::toUnderlyingType(lt::session::global_peer_class_id));

    // IPv6 may not be available on OS and the parsing
    // would result in an exception -> abnormal program termination
    // Affects Windows XP
    try
    {
        f.add_rule(lt::address_v6::any()
                   , lt::make_address("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff")
                   , 1 << LT::toUnderlyingType(lt::session::global_peer_class_id));
    }
    catch (const std::exception &) {}

    if (ignoreLimitsOnLAN())
    {
        // local networks
        f.add_rule(lt::make_address("10.0.0.0")
                   , lt::make_address("10.255.255.255")
                   , 1 << LT::toUnderlyingType(lt::session::local_peer_class_id));
        f.add_rule(lt::make_address("172.16.0.0")
                   , lt::make_address("172.31.255.255")
                   , 1 << LT::toUnderlyingType(lt::session::local_peer_class_id));
        f.add_rule(lt::make_address("192.168.0.0")
                   , lt::make_address("192.168.255.255")
                   , 1 << LT::toUnderlyingType(lt::session::local_peer_class_id));
        // link local
        f.add_rule(lt::make_address("169.254.0.0")
                   , lt::make_address("169.254.255.255")
                   , 1 << LT::toUnderlyingType(lt::session::local_peer_class_id));
        // loopback
        f.add_rule(lt::make_address("127.0.0.0")
                   , lt::make_address("127.255.255.255")
                   , 1 << LT::toUnderlyingType(lt::session::local_peer_class_id));

        // IPv6 may not be available on OS and the parsing
        // would result in an exception -> abnormal program termination
        // Affects Windows XP
        try
        {
            // link local
            f.add_rule(lt::make_address("fe80::")
                       , lt::make_address("febf:ffff:ffff:ffff:ffff:ffff:ffff:ffff")
                       , 1 << LT::toUnderlyingType(lt::session::local_peer_class_id));
            // unique local addresses
            f.add_rule(lt::make_address("fc00::")
                       , lt::make_address("fdff:ffff:ffff:ffff:ffff:ffff:ffff:ffff")
                       , 1 << LT::toUnderlyingType(lt::session::local_peer_class_id));
            // loopback
            f.add_rule(lt::address_v6::loopback()
                       , lt::address_v6::loopback()
                       , 1 << LT::toUnderlyingType(lt::session::local_peer_class_id));
        }
        catch (const std::exception &) {}
    }
    m_nativeSession->set_peer_class_filter(f);

    lt::peer_class_type_filter peerClassTypeFilter;
    peerClassTypeFilter.add(lt::peer_class_type_filter::tcp_socket, lt::session::tcp_peer_class_id);
    peerClassTypeFilter.add(lt::peer_class_type_filter::ssl_tcp_socket, lt::session::tcp_peer_class_id);
    peerClassTypeFilter.add(lt::peer_class_type_filter::i2p_socket, lt::session::tcp_peer_class_id);
    if (!isUTPRateLimited())
    {
        peerClassTypeFilter.disallow(lt::peer_class_type_filter::utp_socket
            , lt::session::global_peer_class_id);
        peerClassTypeFilter.disallow(lt::peer_class_type_filter::ssl_utp_socket
            , lt::session::global_peer_class_id);
    }
    m_nativeSession->set_peer_class_type_filter(peerClassTypeFilter);
}

void SessionImpl::enableTracker(const bool enable)
{
    const QString profile = u"embeddedTracker"_s;
    auto *portForwarder = Net::PortForwarder::instance();

    if (enable)
    {
        if (!m_tracker)
            m_tracker = new Tracker(this);

        m_tracker->start();

        const auto *pref = Preferences::instance();
        if (pref->isTrackerPortForwardingEnabled())
            portForwarder->setPorts(profile, {static_cast<quint16>(pref->getTrackerPort())});
        else
            portForwarder->removePorts(profile);
    }
    else
    {
        delete m_tracker;

        portForwarder->removePorts(profile);
    }
}

void SessionImpl::enableBandwidthScheduler()
{
    if (!m_bwScheduler)
    {
        m_bwScheduler = new BandwidthScheduler(this);
        connect(m_bwScheduler.data(), &BandwidthScheduler::bandwidthLimitRequested
                , this, &SessionImpl::setAltGlobalSpeedLimitEnabled);
    }
    m_bwScheduler->start();
}

void SessionImpl::populateAdditionalTrackers()
{
    m_additionalTrackerEntries = parseTrackerEntries(additionalTrackers());
}

void SessionImpl::populateAdditionalTrackersFromURL()
{
    m_additionalTrackerEntriesFromURL = parseTrackerEntries(additionalTrackersFromURL());
}

void SessionImpl::processTorrentShareLimits(TorrentImpl *torrent)
{
    if (!torrent->isFinished() || torrent->isForced())
        return;

    const qreal ratioLimit = torrent->effectiveRatioLimit();
    const int seedingTimeLimit = torrent->effectiveSeedingTimeLimit();
    const int inactiveSeedingTimeLimit = torrent->effectiveInactiveSeedingTimeLimit();

    bool reached = false;
    QString description;

    if (const qreal ratio = torrent->realRatio();
            (ratioLimit >= 0) && (ratio >= ratioLimit))
    {
        reached = true;
        description = tr("Torrent reached the share ratio limit.");
    }
    else if (const qlonglong seedingTimeInMinutes = torrent->finishedTime() / 60;
            (seedingTimeLimit >= 0) && (seedingTimeInMinutes >= seedingTimeLimit))
    {
        reached = true;
        description = tr("Torrent reached the seeding time limit.");
    }
    else if (const qlonglong inactiveSeedingTimeInMinutes = torrent->timeSinceActivity() / 60;
            (inactiveSeedingTimeLimit >= 0) && (inactiveSeedingTimeInMinutes >= inactiveSeedingTimeLimit))
    {
        reached = true;
        description = tr("Torrent reached the inactive seeding time limit.");
    }

    if (reached)
    {
        const QString torrentName = tr("Torrent: \"%1\".").arg(torrent->name());
        const ShareLimitAction shareLimitAction = torrent->effectiveShareLimitAction();

        if (shareLimitAction == ShareLimitAction::Remove)
        {
            LogMsg(u"%1 %2 %3"_s.arg(description, tr("Removing torrent."), torrentName));
            removeTorrent(torrent->id(), TorrentRemoveOption::KeepContent);
        }
        else if (shareLimitAction == ShareLimitAction::RemoveWithContent)
        {
            LogMsg(u"%1 %2 %3"_s.arg(description, tr("Removing torrent and deleting its content."), torrentName));
            removeTorrent(torrent->id(), TorrentRemoveOption::RemoveContent);
        }
        else if ((shareLimitAction == ShareLimitAction::Stop) && !torrent->isStopped())
        {
            torrent->stop();
            LogMsg(u"%1 %2 %3"_s.arg(description, tr("Torrent stopped."), torrentName));
        }
        else if ((shareLimitAction == ShareLimitAction::EnableSuperSeeding) && !torrent->isStopped() && !torrent->superSeeding())
        {
            torrent->setSuperSeeding(true);
            LogMsg(u"%1 %2 %3"_s.arg(description, tr("Super seeding enabled."), torrentName));
        }
    }
}

void SessionImpl::torrentContentRemovingFinished(const QString &torrentName, const QString &errorMessage)
{
    if (errorMessage.isEmpty())
    {
        LogMsg(tr("Torrent content removed. Torrent: \"%1\"").arg(torrentName));
    }
    else
    {
        LogMsg(tr("Failed to remove torrent content. Torrent: \"%1\". Error: \"%2\"")
            .arg(torrentName, errorMessage), Log::WARNING);
    }
}

Torrent *SessionImpl::getTorrent(const TorrentID &id) const
{
    return m_torrents.value(id);
}

Torrent *SessionImpl::findTorrent(const InfoHash &infoHash) const
{
    const auto id = TorrentID::fromInfoHash(infoHash);
    if (Torrent *torrent = m_torrents.value(id))
        return torrent;

    if (!infoHash.isHybrid())
        return m_hybridTorrentsByAltID.value(id);

    // alternative ID can be useful to find existing torrent
    // in case if hybrid torrent was added by v1 info hash
    const auto altID = TorrentID::fromSHA1Hash(infoHash.v1());
    return m_torrents.value(altID);
}

void SessionImpl::banIP(const QString &ip)
{
    if (m_bannedIPs.get().contains(ip))
        return;

    lt::error_code ec;
    const lt::address addr = lt::make_address(ip.toLatin1().constData(), ec);
    Q_ASSERT(!ec);
    if (ec)
        return;

    invokeAsync([session = m_nativeSession, addr]
    {
        lt::ip_filter filter = session->get_ip_filter();
        filter.add_rule(addr, addr, lt::ip_filter::blocked);
        session->set_ip_filter(std::move(filter));
    });

    QStringList bannedIPs = m_bannedIPs;
    bannedIPs.append(ip);
    bannedIPs.sort();
    m_bannedIPs = bannedIPs;
}

void SessionImpl::shadowbanIP(const QString &ip)
{
    if (m_shadowBannedIPs.get().contains(ip))
        return;

    lt::error_code ec;
    lt::make_address(ip.toLatin1().constData(), ec); // Only check IP valid.
    Q_ASSERT(!ec);
    if (ec)
        return;

    QStringList shadowBannedIPs = m_shadowBannedIPs;
    shadowBannedIPs.append(ip);
    shadowBannedIPs.sort();
    m_shadowBannedIPs = shadowBannedIPs;
}

// Delete a torrent from the session, given its hash
// and from the disk, if the corresponding deleteOption is chosen
bool SessionImpl::removeTorrent(const TorrentID &id, const TorrentRemoveOption deleteOption)
{
    TorrentImpl *const torrent = m_torrents.take(id);
    if (!torrent)
        return false;

    const TorrentID torrentID = torrent->id();
    const QString torrentName = torrent->name();

    qDebug("Deleting torrent with ID: %s", qUtf8Printable(torrentID.toString()));
    emit torrentAboutToBeRemoved(torrent);

    if (const InfoHash infoHash = torrent->infoHash(); infoHash.isHybrid())
        m_hybridTorrentsByAltID.remove(TorrentID::fromSHA1Hash(infoHash.v1()));

    // Remove it from session
    if (deleteOption == TorrentRemoveOption::KeepContent)
    {
        m_removingTorrents[torrentID] = {torrentName, torrent->actualStorageLocation(), {}, deleteOption};

        const lt::torrent_handle nativeHandle {torrent->nativeHandle()};
        const auto iter = std::ranges::find_if(asConst(m_moveStorageQueue)
            , [&nativeHandle](const MoveStorageJob &job)
        {
            return job.torrentHandle == nativeHandle;
        });
        if (iter != m_moveStorageQueue.cend())
        {
            // We shouldn't actually remove torrent until existing "move storage jobs" are done
            torrentQueuePositionBottom(nativeHandle);
            nativeHandle.unset_flags(lt::torrent_flags::auto_managed);
            nativeHandle.pause();
        }
        else
        {
            m_nativeSession->remove_torrent(nativeHandle, lt::session::delete_partfile);
        }
    }
    else
    {
        m_removingTorrents[torrentID] = {torrentName, torrent->actualStorageLocation(), torrent->actualFilePaths(), deleteOption};

        if (m_moveStorageQueue.size() > 1)
        {
            // Delete "move storage job" for the deleted torrent
            // (note: we shouldn't delete active job)
            const auto iter = std::ranges::find_if(std::views::drop(asConst(m_moveStorageQueue), 1)
                , [torrent](const MoveStorageJob &job)
            {
                return job.torrentHandle == torrent->nativeHandle();
            });
            if (iter != m_moveStorageQueue.cend())
                m_moveStorageQueue.erase(iter);
        }

        m_nativeSession->remove_torrent(torrent->nativeHandle(), lt::session::delete_partfile);
    }

    // Remove it from torrent resume directory
    m_resumeDataStorage->remove(torrentID);

    LogMsg(tr("Torrent removed. Torrent: \"%1\"").arg(torrentName));
    delete torrent;
    return true;
}

bool SessionImpl::cancelDownloadMetadata(const TorrentID &id)
{
    const auto downloadedMetadataIter = m_downloadedMetadata.constFind(id);
    if (downloadedMetadataIter == m_downloadedMetadata.cend())
        return false;

    const lt::torrent_handle nativeHandle = downloadedMetadataIter.value();
    m_downloadedMetadata.erase(downloadedMetadataIter);

    if (!nativeHandle.is_valid())
        return true;

#ifdef QBT_USES_LIBTORRENT2
    const InfoHash infoHash = getInfoHash(nativeHandle);
    if (infoHash.isHybrid())
    {
        // if magnet link was hybrid initially then it is indexed also by v1 info hash
        // so we need to remove both entries
        const auto altID = TorrentID::fromSHA1Hash(infoHash.v1());
        m_downloadedMetadata.remove(altID);
    }
#endif

    m_nativeSession->remove_torrent(nativeHandle);
    return true;
}

void SessionImpl::increaseTorrentsQueuePos(const QList<TorrentID> &ids)
{
    // Increase torrents queue position (starting with the one in the highest queue position)
    for (TorrentImpl *torrent : asConst(getQueuedTorrentsByID(ids)))
        torrentQueuePositionUp(torrent->nativeHandle());

    m_torrentsQueueChanged = true;
}

void SessionImpl::decreaseTorrentsQueuePos(const QList<TorrentID> &ids)
{
    const QList<TorrentImpl *> queuedTorrents = getQueuedTorrentsByID(ids);

    // Decrease torrents queue position (starting with the one in the lowest queue position)
    for (const TorrentImpl *torrent : std::views::reverse(queuedTorrents))
        torrentQueuePositionDown(torrent->nativeHandle());

    for (const lt::torrent_handle &torrentHandle : asConst(m_downloadedMetadata))
        torrentQueuePositionBottom(torrentHandle);

    m_torrentsQueueChanged = true;
}

void SessionImpl::topTorrentsQueuePos(const QList<TorrentID> &ids)
{
    const QList<TorrentImpl *> queuedTorrents = getQueuedTorrentsByID(ids);

    // Top torrents queue position (starting with the one in the lowest queue position)
    for (const TorrentImpl *torrent : std::views::reverse(queuedTorrents))
        torrentQueuePositionTop(torrent->nativeHandle());

    m_torrentsQueueChanged = true;
}

void SessionImpl::bottomTorrentsQueuePos(const QList<TorrentID> &ids)
{
    // Bottom torrents queue position (starting with the one in the highest queue position)
    for (TorrentImpl *torrent : asConst(getQueuedTorrentsByID(ids)))
        torrentQueuePositionBottom(torrent->nativeHandle());

    for (const lt::torrent_handle &torrentHandle : asConst(m_downloadedMetadata))
        torrentQueuePositionBottom(torrentHandle);

    m_torrentsQueueChanged = true;
}

void SessionImpl::handleTorrentResumeDataRequested(const TorrentImpl *torrent)
{
    qDebug("Saving resume data is requested for torrent '%s'...", qUtf8Printable(torrent->name()));
    ++m_numResumeData;
}

QList<Torrent *> SessionImpl::torrents() const
{
    QList<Torrent *> result;
    result.reserve(m_torrents.size());
    for (TorrentImpl *torrent : asConst(m_torrents))
        result << torrent;

    return result;
}

qsizetype SessionImpl::torrentsCount() const
{
    return m_torrents.size();
}

bool SessionImpl::addTorrent(const TorrentDescriptor &torrentDescr, const AddTorrentParams &params)
{
    if (!isRestored())
        return false;

    return addTorrent_impl(torrentDescr, params);
}

LoadTorrentParams SessionImpl::initLoadTorrentParams(const AddTorrentParams &addTorrentParams)
{
    LoadTorrentParams loadTorrentParams;

    loadTorrentParams.name = addTorrentParams.name;
    loadTorrentParams.firstLastPiecePriority = addTorrentParams.firstLastPiecePriority;
    loadTorrentParams.hasFinishedStatus = addTorrentParams.skipChecking; // do not react on 'torrent_finished_alert' when skipping
    loadTorrentParams.contentLayout = addTorrentParams.contentLayout.value_or(torrentContentLayout());
    loadTorrentParams.operatingMode = (addTorrentParams.addForced ? TorrentOperatingMode::Forced : TorrentOperatingMode::AutoManaged);
    loadTorrentParams.stopped = addTorrentParams.addStopped.value_or(isAddTorrentStopped());
    loadTorrentParams.stopCondition = addTorrentParams.stopCondition.value_or(torrentStopCondition());
    loadTorrentParams.addToQueueTop = addTorrentParams.addToQueueTop.value_or(isAddTorrentToQueueTop());
    loadTorrentParams.ratioLimit = addTorrentParams.ratioLimit;
    loadTorrentParams.seedingTimeLimit = addTorrentParams.seedingTimeLimit;
    loadTorrentParams.inactiveSeedingTimeLimit = addTorrentParams.inactiveSeedingTimeLimit;
    loadTorrentParams.shareLimitAction = addTorrentParams.shareLimitAction;
    loadTorrentParams.sslParameters = addTorrentParams.sslParameters;

    const QString category = addTorrentParams.category;
    if (!category.isEmpty() && !m_categories.contains(category) && !addCategory(category))
        loadTorrentParams.category = u""_s;
    else
        loadTorrentParams.category = category;

    const auto defaultSavePath = suggestedSavePath(loadTorrentParams.category, addTorrentParams.useAutoTMM);
    const auto defaultDownloadPath = suggestedDownloadPath(loadTorrentParams.category, addTorrentParams.useAutoTMM);

    loadTorrentParams.useAutoTMM = addTorrentParams.useAutoTMM.value_or(
            addTorrentParams.savePath.isEmpty() && addTorrentParams.downloadPath.isEmpty() && !isAutoTMMDisabledByDefault());

    if (!loadTorrentParams.useAutoTMM)
    {
        if (addTorrentParams.savePath.isAbsolute())
            loadTorrentParams.savePath = addTorrentParams.savePath;
        else
            loadTorrentParams.savePath = defaultSavePath / addTorrentParams.savePath;

        // if useDownloadPath isn't specified but downloadPath is explicitly set we prefer to use it
        const bool useDownloadPath = addTorrentParams.useDownloadPath.value_or(!addTorrentParams.downloadPath.isEmpty() || isDownloadPathEnabled());
        if (useDownloadPath)
        {
            // Overridden "Download path" settings

            if (addTorrentParams.downloadPath.isAbsolute())
            {
                loadTorrentParams.downloadPath = addTorrentParams.downloadPath;
            }
            else
            {
                const Path basePath = (!defaultDownloadPath.isEmpty() ? defaultDownloadPath : downloadPath());
                loadTorrentParams.downloadPath = basePath / addTorrentParams.downloadPath;
            }
        }
    }

    for (const Tag &tag : addTorrentParams.tags)
    {
        if (hasTag(tag) || addTag(tag))
            loadTorrentParams.tags.insert(tag);
    }

    return loadTorrentParams;
}

// Add a torrent to the BitTorrent session
bool SessionImpl::addTorrent_impl(const TorrentDescriptor &source, const AddTorrentParams &addTorrentParams)
{
    Q_ASSERT(isRestored());

    const bool hasMetadata = (source.info().has_value());
    const auto infoHash = source.infoHash();
    const auto id = TorrentID::fromInfoHash(infoHash);

    // alternative ID can be useful to find existing torrent in case if hybrid torrent was added by v1 info hash
    const auto altID = (infoHash.isHybrid() ? TorrentID::fromSHA1Hash(infoHash.v1()) : TorrentID());

    if (Torrent *torrent = findTorrent(infoHash))
    {
        // a duplicate torrent is being added

        if (hasMetadata)
        {
            // Trying to set metadata to existing torrent in case if it has none
            torrent->setMetadata(*source.info());
        }

        if (!isMergeTrackersEnabled())
        {
            const QString message = tr("Merging of trackers is disabled");
            LogMsg(tr("Detected an attempt to add a duplicate torrent. Existing torrent: \"%1\". Torrent infohash: %2. Result: %3")
                    .arg(torrent->name(), torrent->infoHash().toString(), message));
            emit addTorrentFailed(infoHash, {AddTorrentError::DuplicateTorrent, message});
            return false;
        }

        const bool isPrivate = torrent->isPrivate() || (hasMetadata && source.info()->isPrivate());
        if (isPrivate)
        {
            const QString message = tr("Trackers cannot be merged because it is a private torrent");
            LogMsg(tr("Detected an attempt to add a duplicate torrent. Existing torrent: \"%1\". Torrent infohash: %2. Result: %3")
                    .arg(torrent->name(), torrent->infoHash().toString(), message));
            emit addTorrentFailed(infoHash, {AddTorrentError::DuplicateTorrent, message});
            return false;
        }

        // merge trackers and web seeds
        torrent->addTrackers(source.trackers());
        torrent->addUrlSeeds(source.urlSeeds());

        const QString message = tr("Trackers are merged from new source");
        LogMsg(tr("Detected an attempt to add a duplicate torrent. Existing torrent: \"%1\". Torrent infohash: %2. Result: %3")
                .arg(torrent->name(), torrent->infoHash().toString(), message));
        emit addTorrentFailed(infoHash, {AddTorrentError::DuplicateTorrent, message});
        return false;
    }

    // It looks illogical that we don't just use an existing handle,
    // but as previous experience has shown, it actually creates unnecessary
    // problems and unwanted behavior due to the fact that it was originally
    // added with parameters other than those provided by the user.
    cancelDownloadMetadata(id);
    if (infoHash.isHybrid())
        cancelDownloadMetadata(altID);

    LoadTorrentParams loadTorrentParams = initLoadTorrentParams(addTorrentParams);
    lt::add_torrent_params &p = loadTorrentParams.ltAddTorrentParams;
    p = source.ltAddTorrentParams();

    const bool useAutoTMM = loadTorrentParams.useAutoTMM;
    const Path actualSavePath = useAutoTMM ? categorySavePath(loadTorrentParams.category) : loadTorrentParams.savePath;

    bool needFindIncompleteFiles = false;
    PathList filePaths;

    if (hasMetadata)
    {
        // Torrent  that is being added with metadata is considered to be added as stopped
        // if "metadata received" stop condition is set for it.
        if (loadTorrentParams.stopCondition == Torrent::StopCondition::MetadataReceived)
        {
            loadTorrentParams.stopped = true;
            loadTorrentParams.stopCondition = Torrent::StopCondition::None;
        }

        const TorrentInfo &torrentInfo = *source.info();

        Q_ASSERT(addTorrentParams.filePaths.isEmpty() || (addTorrentParams.filePaths.size() == torrentInfo.filesCount()));

        filePaths = addTorrentParams.filePaths;
        if (filePaths.isEmpty())
        {
            filePaths = torrentInfo.filePaths();
            if (loadTorrentParams.contentLayout != TorrentContentLayout::Original)
            {
                const Path originalRootFolder = Path::findRootFolder(filePaths);
                const auto originalContentLayout = (originalRootFolder.isEmpty()
                        ? TorrentContentLayout::NoSubfolder : TorrentContentLayout::Subfolder);
                if (loadTorrentParams.contentLayout != originalContentLayout)
                {
                    if (loadTorrentParams.contentLayout == TorrentContentLayout::NoSubfolder)
                        Path::stripRootFolder(filePaths);
                    else
                        Path::addRootFolder(filePaths, filePaths.at(0).removedExtension());
                }
            }
        }

        // if torrent name wasn't explicitly set we handle the case of
        // initial renaming of torrent content and rename torrent accordingly
        if (loadTorrentParams.name.isEmpty())
        {
            QString contentName = Path::findRootFolder(filePaths).toString();
            if (contentName.isEmpty() && (filePaths.size() == 1))
                contentName = filePaths.at(0).filename();

            if (!contentName.isEmpty() && (contentName != torrentInfo.name()))
                loadTorrentParams.name = contentName;
        }

        const auto nativeIndexes = torrentInfo.nativeIndexes();

        Q_ASSERT(addTorrentParams.filePriorities.isEmpty() || (addTorrentParams.filePriorities.size() == nativeIndexes.size()));
        QList<DownloadPriority> filePriorities = addTorrentParams.filePriorities;

        // Filename filter should be applied before `findIncompleteFiles()` is called.
        if (filePriorities.isEmpty() && isExcludedFileNamesEnabled())
        {
            // Check file name blacklist when priorities are not explicitly set
            applyFilenameFilter(filePaths, filePriorities);
        }

        if (!loadTorrentParams.hasFinishedStatus)
            needFindIncompleteFiles = true;

        const int internalFilesCount = torrentInfo.nativeInfo()->files().num_files(); // including .pad files
        // Use qBittorrent default priority rather than libtorrent's (4)
        p.file_priorities = std::vector(internalFilesCount, LT::toNative(DownloadPriority::Normal));

        if (!filePriorities.isEmpty())
        {
            for (qsizetype i = 0; i < filePriorities.size(); ++i)
                p.file_priorities[LT::toUnderlyingType(nativeIndexes[i])] = LT::toNative(filePriorities[i]);
        }

        Q_ASSERT(p.ti);
    }
    else
    {
        if (loadTorrentParams.name.isEmpty() && !p.name.empty())
            loadTorrentParams.name = QString::fromStdString(p.name);
    }

    if (isAddTrackersEnabled() && !(hasMetadata && p.ti->priv()))
    {
        const auto maxTierIter = std::ranges::max_element(asConst(p.tracker_tiers));
        const int baseTier = (maxTierIter != p.tracker_tiers.cend()) ? (*maxTierIter + 1) : 0;

        p.trackers.reserve(p.trackers.size() + static_cast<std::size_t>(m_additionalTrackerEntries.size()));
        p.tracker_tiers.reserve(p.trackers.size() + static_cast<std::size_t>(m_additionalTrackerEntries.size()));
        p.tracker_tiers.resize(p.trackers.size(), 0);
        for (const TrackerEntry &trackerEntry : asConst(m_additionalTrackerEntries))
        {
            p.trackers.emplace_back(trackerEntry.url.toStdString());
            p.tracker_tiers.emplace_back(Utils::Number::clampingAdd(trackerEntry.tier, baseTier));
        }
    }

    if (isAddTrackersFromURLEnabled() && !(hasMetadata && p.ti->priv()))
    {
        const auto maxTierIter = std::ranges::max_element(asConst(p.tracker_tiers));
        const int baseTier = (maxTierIter != p.tracker_tiers.cend()) ? (*maxTierIter + 1) : 0;

        p.trackers.reserve(p.trackers.size() + static_cast<std::size_t>(m_additionalTrackerEntriesFromURL.size()));
        p.tracker_tiers.reserve(p.trackers.size() + static_cast<std::size_t>(m_additionalTrackerEntriesFromURL.size()));
        p.tracker_tiers.resize(p.trackers.size(), 0);
        for (const TrackerEntry &trackerEntry : asConst(m_additionalTrackerEntriesFromURL))
        {
            p.trackers.emplace_back(trackerEntry.url.toStdString());
            p.tracker_tiers.emplace_back(Utils::Number::clampingAdd(trackerEntry.tier, baseTier));
        }
    }

    p.upload_limit = addTorrentParams.uploadLimit;
    p.download_limit = addTorrentParams.downloadLimit;

    // Preallocation mode
    p.storage_mode = isPreallocationEnabled() ? lt::storage_mode_allocate : lt::storage_mode_sparse;

    if (addTorrentParams.sequential)
        p.flags |= lt::torrent_flags::sequential_download;
    else
        p.flags &= ~lt::torrent_flags::sequential_download;

    // Seeding mode
    // Skip checking and directly start seeding
    if (addTorrentParams.skipChecking)
        p.flags |= lt::torrent_flags::seed_mode;
    else
        p.flags &= ~lt::torrent_flags::seed_mode;

    if (loadTorrentParams.stopped || (loadTorrentParams.operatingMode == TorrentOperatingMode::AutoManaged))
        p.flags |= lt::torrent_flags::paused;
    else
        p.flags &= ~lt::torrent_flags::paused;
    if (loadTorrentParams.stopped || (loadTorrentParams.operatingMode == TorrentOperatingMode::Forced))
        p.flags &= ~lt::torrent_flags::auto_managed;
    else
        p.flags |= lt::torrent_flags::auto_managed;

    p.flags |= lt::torrent_flags::duplicate_is_error;

    p.added_time = std::time(nullptr);

    // Limits
    p.max_connections = maxConnectionsPerTorrent();
    p.max_uploads = maxUploadsPerTorrent();

    p.userdata = LTClientData(new ExtensionData);
#ifndef QBT_USES_LIBTORRENT2
    p.storage = customStorageConstructor;
#endif

    const auto resolveFileNames = [&, this]
    {
        if (!needFindIncompleteFiles)
            return QtFuture::makeReadyValueFuture(FileSearchResult {.savePath = actualSavePath, .fileNames = filePaths});

        const Path actualDownloadPath = loadTorrentParams.useAutoTMM
                ? categoryDownloadPath(loadTorrentParams.category) : loadTorrentParams.downloadPath;
        return findIncompleteFiles(actualSavePath, actualDownloadPath, filePaths);
    };

    resolveFileNames().then(this, [this, id, loadTorrentParams = std::move(loadTorrentParams)](const FileSearchResult &result) mutable
    {
        lt::add_torrent_params &p = loadTorrentParams.ltAddTorrentParams;

        p.save_path = result.savePath.toString().toStdString();
        if (p.ti)
        {
            const TorrentInfo torrentInfo {*p.ti};
            const auto nativeIndexes = torrentInfo.nativeIndexes();
            for (qsizetype i = 0; i < result.fileNames.size(); ++i)
                p.renamed_files[nativeIndexes[i]] = result.fileNames[i].toString().toStdString();
        }

        m_nativeSession->async_add_torrent(p);
        m_addTorrentAlertHandlers.append([this, loadTorrentParams = std::move(loadTorrentParams)](const lt::add_torrent_alert *alert) mutable
        {
            if (alert->error)
            {
                const QString msg = QString::fromStdString(alert->message());
                LogMsg(tr("Failed to add torrent. Reason: \"%1\"").arg(msg), Log::WARNING);

                const InfoHash infoHash = getInfoHash(alert->params);
                const AddTorrentError::Kind errorKind = (alert->error == lt::errors::duplicate_torrent)
                        ? AddTorrentError::DuplicateTorrent : AddTorrentError::Other;
                emit addTorrentFailed(infoHash, {errorKind, msg});
            }
            else
            {
                if (loadTorrentParams.addToQueueTop)
                    alert->handle.queue_position_top();

                TorrentImpl *torrent = createTorrent(alert->handle, std::move(loadTorrentParams));
                m_loadedTorrents.append(torrent);

                torrent->requestResumeData(lt::torrent_handle::save_info_dict);

                LogMsg(tr("Added new torrent. Torrent: \"%1\"").arg(torrent->name()));
                emit torrentAdded(torrent);

                // The following is useless for newly added magnet
                if (torrent->hasMetadata())
                {
                    if (!torrentExportDirectory().isEmpty())
                        exportTorrentFile(torrent, torrentExportDirectory());
                }
            }
        });
    });

    return true;
}

QFuture<FileSearchResult> SessionImpl::findIncompleteFiles(const Path &savePath, const Path &downloadPath, const PathList &filePaths) const
{
    QPromise<FileSearchResult> promise;
    QFuture<FileSearchResult> future = promise.future();
    promise.start();
    QMetaObject::invokeMethod(m_fileSearcher, [=, this, promise = std::move(promise)]() mutable
    {
        m_fileSearcher->search(filePaths, savePath, downloadPath, isAppendExtensionEnabled(), promise);
        promise.finish();
    });

    return future;
}

void SessionImpl::enablePortMapping()
{
    invokeAsync([this]
    {
        if (m_isPortMappingEnabled)
            return;

        lt::settings_pack settingsPack;
        settingsPack.set_bool(lt::settings_pack::enable_upnp, true);
        settingsPack.set_bool(lt::settings_pack::enable_natpmp, true);
        m_nativeSession->apply_settings(std::move(settingsPack));

        m_isPortMappingEnabled = true;

        LogMsg(tr("UPnP/NAT-PMP support: ON"), Log::INFO);
    });
}

void SessionImpl::disablePortMapping()
{
    invokeAsync([this]
    {
        if (!m_isPortMappingEnabled)
            return;

        lt::settings_pack settingsPack;
        settingsPack.set_bool(lt::settings_pack::enable_upnp, false);
        settingsPack.set_bool(lt::settings_pack::enable_natpmp, false);
        m_nativeSession->apply_settings(std::move(settingsPack));

        m_mappedPorts.clear();
        m_isPortMappingEnabled = false;

        LogMsg(tr("UPnP/NAT-PMP support: OFF"), Log::INFO);
    });
}

void SessionImpl::addMappedPorts(const QSet<quint16> &ports)
{
    invokeAsync([this, ports]
    {
        if (!m_isPortMappingEnabled)
            return;

        for (const quint16 port : ports)
        {
            if (!m_mappedPorts.contains(port))
                m_mappedPorts.insert(port, m_nativeSession->add_port_mapping(lt::session::tcp, port, port));
        }
    });
}

void SessionImpl::removeMappedPorts(const QSet<quint16> &ports)
{
    invokeAsync([this, ports]
    {
        if (!m_isPortMappingEnabled)
            return;

        Algorithm::removeIf(m_mappedPorts, [this, ports](const quint16 port, const std::vector<lt::port_mapping_t> &handles)
        {
            if (!ports.contains(port))
                return false;

            for (const lt::port_mapping_t &handle : handles)
                m_nativeSession->delete_port_mapping(handle);

            return true;
        });
    });
}

// Add a torrent to libtorrent session in hidden mode
// and force it to download its metadata
bool SessionImpl::downloadMetadata(const TorrentDescriptor &torrentDescr)
{
    Q_ASSERT(!torrentDescr.info().has_value());
    if (torrentDescr.info().has_value()) [[unlikely]]
        return false;

    const InfoHash infoHash = torrentDescr.infoHash();

    // We should not add torrent if it's already
    // processed or adding to session
    if (isKnownTorrent(infoHash))
        return false;

    lt::add_torrent_params p = torrentDescr.ltAddTorrentParams();

    if (isAddTrackersEnabled())
    {
        // Use "additional trackers" when metadata retrieving (this can help when the DHT nodes are few)

        const auto maxTierIter = std::ranges::max_element(asConst(p.tracker_tiers));
        const int baseTier = (maxTierIter != p.tracker_tiers.cend()) ? (*maxTierIter + 1) : 0;

        p.trackers.reserve(p.trackers.size() + static_cast<std::size_t>(m_additionalTrackerEntries.size()));
        p.tracker_tiers.reserve(p.trackers.size() + static_cast<std::size_t>(m_additionalTrackerEntries.size()));
        p.tracker_tiers.resize(p.trackers.size(), 0);
        for (const TrackerEntry &trackerEntry : asConst(m_additionalTrackerEntries))
        {
            p.trackers.emplace_back(trackerEntry.url.toStdString());
            p.tracker_tiers.emplace_back(Utils::Number::clampingAdd(trackerEntry.tier, baseTier));
        }
    }

    // Flags
    // Preallocation mode
    if (isPreallocationEnabled())
        p.storage_mode = lt::storage_mode_allocate;
    else
        p.storage_mode = lt::storage_mode_sparse;

    // Limits
    p.max_connections = maxConnectionsPerTorrent();
    p.max_uploads = maxUploadsPerTorrent();

    const auto id = TorrentID::fromInfoHash(infoHash);
    const Path savePath = Utils::Fs::tempPath() / Path(id.toString());
    p.save_path = savePath.toString().toStdString();

    // Forced start
    p.flags &= ~lt::torrent_flags::paused;
    p.flags &= ~lt::torrent_flags::auto_managed;

    // Solution to avoid accidental file writes
    p.flags |= lt::torrent_flags::upload_mode;

#ifndef QBT_USES_LIBTORRENT2
    p.storage = customStorageConstructor;
#endif

    // Adding torrent to libtorrent session
    m_nativeSession->async_add_torrent(p);
    m_downloadedMetadata.insert(id, {});
    m_addTorrentAlertHandlers.append([this](const lt::add_torrent_alert *alert)
    {
        if (alert->error)
        {
            const QString msg = QString::fromStdString(alert->message());
            LogMsg(tr("Failed to download torrent metadata. Reason: \"%1\"").arg(msg), Log::WARNING);

            m_downloadedMetadata.remove(getInfoHash(alert->params).toTorrentID());
        }
        else
        {
            const InfoHash infoHash = getInfoHash(alert->handle);
            const auto torrentID = TorrentID::fromInfoHash(infoHash);

            if (const auto downloadedMetadataIter = m_downloadedMetadata.find(torrentID)
                ; downloadedMetadataIter != m_downloadedMetadata.end())
            {
                downloadedMetadataIter.value() = alert->handle;
                if (infoHash.isHybrid())
                {
                    // index hybrid magnet links by both v1 and v2 info hashes
                    const auto altID = TorrentID::fromSHA1Hash(infoHash.v1());
                    m_downloadedMetadata[altID] = alert->handle;
                }
            }
        }
    });

    return true;
}

void SessionImpl::exportTorrentFile(const Torrent *torrent, const Path &folderPath)
{
    if (!folderPath.exists() && !Utils::Fs::mkpath(folderPath))
        return;

    const QString validName = Utils::Fs::toValidFileName(torrent->name());
    QString torrentExportFilename = u"%1.torrent"_s.arg(validName);
    Path newTorrentPath = folderPath / Path(torrentExportFilename);
    int counter = 0;
    while (newTorrentPath.exists())
    {
        // Append number to torrent name to make it unique
        torrentExportFilename = u"%1 (%2).torrent"_s.arg(validName).arg(++counter);
        newTorrentPath = folderPath / Path(torrentExportFilename);
    }

    const nonstd::expected<void, QString> result = torrent->exportToFile(newTorrentPath);
    if (!result)
    {
        LogMsg(tr("Failed to export torrent. Torrent: \"%1\". Destination: \"%2\". Reason: \"%3\"")
               .arg(torrent->name(), newTorrentPath.toString(), result.error()), Log::WARNING);
    }
}

void SessionImpl::generateResumeData()
{
    for (TorrentImpl *const torrent : asConst(m_torrents))
    {
        if (torrent->needSaveResumeData())
            torrent->requestResumeData();
    }
}

// Called on exit
void SessionImpl::saveResumeData()
{
    for (TorrentImpl *torrent : asConst(m_torrents))
    {
        // When the session is terminated due to unrecoverable error
        // some of the torrent handles can be corrupted
        try
        {
            torrent->requestResumeData(lt::torrent_handle::only_if_modified);
        }
        catch (const std::exception &) {}
    }

    // clear queued storage move jobs except the current ongoing one
    if (m_moveStorageQueue.size() > 1)
        m_moveStorageQueue.resize(1);

    QElapsedTimer timer;
    timer.start();

    while ((m_numResumeData > 0) || !m_moveStorageQueue.isEmpty() || m_needSaveTorrentsQueue)
    {
        const lt::seconds waitTime {5};
        const lt::seconds expireTime {30};

        // only terminate when no storage is moving
        if (timer.hasExpired(lt::total_milliseconds(expireTime)) && m_moveStorageQueue.isEmpty())
        {
            LogMsg(tr("Aborted saving resume data. Number of outstanding torrents: %1").arg(QString::number(m_numResumeData))
                , Log::CRITICAL);
            break;
        }

        fetchPendingAlerts(waitTime);

        bool hasWantedAlert = false;
        for (lt::alert *alert : m_alerts)
        {
            if (const int alertType = alert->type();
                (alertType == lt::save_resume_data_alert::alert_type) || (alertType == lt::save_resume_data_failed_alert::alert_type)
                || (alertType == lt::storage_moved_alert::alert_type) || (alertType == lt::storage_moved_failed_alert::alert_type)
                || (alertType == lt::state_update_alert::alert_type))
            {
                hasWantedAlert = true;
            }

            handleAlert(alert);
        }

        if (hasWantedAlert)
            timer.start();
    }
}

void SessionImpl::saveTorrentsQueue()
{
    QList<TorrentID> queue;
    for (const TorrentImpl *torrent : asConst(m_torrents))
    {
        if (const int queuePos = torrent->queuePosition(); queuePos >= 0)
        {
            if (queuePos >= queue.size())
                queue.resize(queuePos + 1);
            queue[queuePos] = torrent->id();
        }
    }

    m_resumeDataStorage->storeQueue(queue);
    m_needSaveTorrentsQueue = false;
}

void SessionImpl::removeTorrentsQueue()
{
    m_resumeDataStorage->storeQueue({});
    m_torrentsQueueChanged = false;
    m_needSaveTorrentsQueue = false;
}

void SessionImpl::setSavePath(const Path &path)
{
    const auto newPath = (path.isAbsolute() ? path : (specialFolderLocation(SpecialFolder::Downloads) / path));
    if (newPath == m_savePath)
        return;

    if (isDisableAutoTMMWhenDefaultSavePathChanged())
    {
        // This should be done before changing the save path
        // to prevent the torrent from being moved at the new save path.

        QSet<QString> affectedCatogories {{}}; // includes default (unnamed) category
        for (auto it = m_categories.cbegin(); it != m_categories.cend(); ++it)
        {
            const QString &categoryName = it.key();
            const CategoryOptions &categoryOptions = it.value();
            if (categoryOptions.savePath.isRelative())
                affectedCatogories.insert(categoryName);
        }

        for (TorrentImpl *const torrent : asConst(m_torrents))
        {
            if (affectedCatogories.contains(torrent->category()))
                torrent->setAutoTMMEnabled(false);
        }
    }

    m_savePath = newPath;
    for (TorrentImpl *const torrent : asConst(m_torrents))
        torrent->handleCategoryOptionsChanged();

    m_freeDiskSpace = -1;
    m_freeDiskSpaceCheckingTimer->stop();
    QMetaObject::invokeMethod(m_freeDiskSpaceChecker, [checker = m_freeDiskSpaceChecker, pathToCheck = m_savePath]
    {
        checker->setPathToCheck(pathToCheck);
        checker->check();
    });
}

void SessionImpl::setDownloadPath(const Path &path)
{
    const Path newPath = (path.isAbsolute() ? path : (savePath() / Path(u"temp"_s) / path));
    if (newPath == m_downloadPath)
        return;

    if (isDisableAutoTMMWhenDefaultSavePathChanged())
    {
        // This should be done before changing the save path
        // to prevent the torrent from being moved at the new save path.

        QSet<QString> affectedCatogories {{}}; // includes default (unnamed) category
        for (auto it = m_categories.cbegin(); it != m_categories.cend(); ++it)
        {
            const QString &categoryName = it.key();
            const CategoryOptions &categoryOptions = it.value();
            const DownloadPathOption downloadPathOption =
                    categoryOptions.downloadPath.value_or(DownloadPathOption {isDownloadPathEnabled(), downloadPath()});
            if (downloadPathOption.enabled && downloadPathOption.path.isRelative())
                affectedCatogories.insert(categoryName);
        }

        for (TorrentImpl *const torrent : asConst(m_torrents))
        {
            if (affectedCatogories.contains(torrent->category()))
                torrent->setAutoTMMEnabled(false);
        }
    }

    m_downloadPath = newPath;
    for (TorrentImpl *const torrent : asConst(m_torrents))
        torrent->handleCategoryOptionsChanged();
}

QStringList SessionImpl::getListeningIPs() const
{
    QStringList IPs;

    const QString ifaceName = networkInterface();
    const QString ifaceAddr = networkInterfaceAddress();
    const QHostAddress configuredAddr(ifaceAddr);
    const bool allIPv4 = (ifaceAddr == u"0.0.0.0"); // Means All IPv4 addresses
    const bool allIPv6 = (ifaceAddr == u"::"); // Means All IPv6 addresses

    if (!ifaceAddr.isEmpty() && !allIPv4 && !allIPv6 && configuredAddr.isNull())
    {
        LogMsg(tr("The configured network address is invalid. Address: \"%1\"").arg(ifaceAddr), Log::CRITICAL);
        // Pass the invalid user configured interface name/address to libtorrent
        // in hopes that it will come online later.
        // This will not cause IP leak but allow user to reconnect the interface
        // and re-establish connection without restarting the client.
        IPs.append(ifaceAddr);
        return IPs;
    }

    if (ifaceName.isEmpty())
    {
        if (ifaceAddr.isEmpty())
            return {u"0.0.0.0"_s, u"::"_s}; // Indicates all interfaces + all addresses (aka default)

        if (allIPv4)
            return {u"0.0.0.0"_s};

        if (allIPv6)
            return {u"::"_s};
    }

    const auto checkAndAddIP = [allIPv4, allIPv6, &IPs](const QHostAddress &addr, const QHostAddress &match)
    {
        if ((allIPv4 && (addr.protocol() != QAbstractSocket::IPv4Protocol))
            || (allIPv6 && (addr.protocol() != QAbstractSocket::IPv6Protocol)))
            return;

        if ((match == addr) || allIPv4 || allIPv6)
            IPs.append(addr.toString());
    };

    if (ifaceName.isEmpty())
    {
        const QList<QHostAddress> addresses = QNetworkInterface::allAddresses();
        for (const auto &addr : addresses)
            checkAndAddIP(addr, configuredAddr);

        // At this point ifaceAddr was non-empty
        // If IPs.isEmpty() it means the configured Address was not found
        if (IPs.isEmpty())
        {
            LogMsg(tr("Failed to find the configured network address to listen on. Address: \"%1\"")
                .arg(ifaceAddr), Log::CRITICAL);
            IPs.append(ifaceAddr);
        }

        return IPs;
    }

    // Attempt to listen on provided interface
    const QNetworkInterface networkIFace = QNetworkInterface::interfaceFromName(ifaceName);
    if (!networkIFace.isValid())
    {
        qDebug("Invalid network interface: %s", qUtf8Printable(ifaceName));
        LogMsg(tr("The configured network interface is invalid. Interface: \"%1\"").arg(ifaceName), Log::CRITICAL);
        IPs.append(ifaceName);
        return IPs;
    }

    if (ifaceAddr.isEmpty())
    {
        IPs.append(ifaceName);
        return IPs; // On Windows calling code converts it to GUID
    }

    const QList<QNetworkAddressEntry> addresses = networkIFace.addressEntries();
    qDebug() << "This network interface has " << addresses.size() << " IP addresses";
    for (const QNetworkAddressEntry &entry : addresses)
        checkAndAddIP(entry.ip(), configuredAddr);

    // Make sure there is at least one IP
    // At this point there was an explicit interface and an explicit address set
    // and the address should have been found
    if (IPs.isEmpty())
    {
        LogMsg(tr("Failed to find the configured network address to listen on. Address: \"%1\"")
            .arg(ifaceAddr), Log::CRITICAL);
        IPs.append(ifaceAddr);
    }

    return IPs;
}

// Set the ports range in which is chosen the port
// the BitTorrent session will listen to
void SessionImpl::configureListeningInterface()
{
    m_listenInterfaceConfigured = false;
    configureDeferred();
}

int SessionImpl::globalDownloadSpeedLimit() const
{
    // Unfortunately the value was saved as KiB instead of B.
    // But it is better to pass it around internally(+ webui) as Bytes.
    return m_globalDownloadSpeedLimit * 1024;
}

void SessionImpl::setGlobalDownloadSpeedLimit(const int limit)
{
    // Unfortunately the value was saved as KiB instead of B.
    // But it is better to pass it around internally(+ webui) as Bytes.
    if (limit == globalDownloadSpeedLimit())
        return;

    if (limit <= 0)
        m_globalDownloadSpeedLimit = 0;
    else if (limit <= 1024)
        m_globalDownloadSpeedLimit = 1;
    else
        m_globalDownloadSpeedLimit = (limit / 1024);

    if (!isAltGlobalSpeedLimitEnabled())
        configureDeferred();
}

int SessionImpl::globalUploadSpeedLimit() const
{
    // Unfortunately the value was saved as KiB instead of B.
    // But it is better to pass it around internally(+ webui) as Bytes.
    return m_globalUploadSpeedLimit * 1024;
}

void SessionImpl::setGlobalUploadSpeedLimit(const int limit)
{
    // Unfortunately the value was saved as KiB instead of B.
    // But it is better to pass it around internally(+ webui) as Bytes.
    if (limit == globalUploadSpeedLimit())
        return;

    if (limit <= 0)
        m_globalUploadSpeedLimit = 0;
    else if (limit <= 1024)
        m_globalUploadSpeedLimit = 1;
    else
        m_globalUploadSpeedLimit = (limit / 1024);

    if (!isAltGlobalSpeedLimitEnabled())
        configureDeferred();
}

int SessionImpl::altGlobalDownloadSpeedLimit() const
{
    // Unfortunately the value was saved as KiB instead of B.
    // But it is better to pass it around internally(+ webui) as Bytes.
    return m_altGlobalDownloadSpeedLimit * 1024;
}

void SessionImpl::setAltGlobalDownloadSpeedLimit(const int limit)
{
    // Unfortunately the value was saved as KiB instead of B.
    // But it is better to pass it around internally(+ webui) as Bytes.
    if (limit == altGlobalDownloadSpeedLimit())
        return;

    if (limit <= 0)
        m_altGlobalDownloadSpeedLimit = 0;
    else if (limit <= 1024)
        m_altGlobalDownloadSpeedLimit = 1;
    else
        m_altGlobalDownloadSpeedLimit = (limit / 1024);

    if (isAltGlobalSpeedLimitEnabled())
        configureDeferred();
}

int SessionImpl::altGlobalUploadSpeedLimit() const
{
    // Unfortunately the value was saved as KiB instead of B.
    // But it is better to pass it around internally(+ webui) as Bytes.
    return m_altGlobalUploadSpeedLimit * 1024;
}

void SessionImpl::setAltGlobalUploadSpeedLimit(const int limit)
{
    // Unfortunately the value was saved as KiB instead of B.
    // But it is better to pass it around internally(+ webui) as Bytes.
    if (limit == altGlobalUploadSpeedLimit())
        return;

    if (limit <= 0)
        m_altGlobalUploadSpeedLimit = 0;
    else if (limit <= 1024)
        m_altGlobalUploadSpeedLimit = 1;
    else
        m_altGlobalUploadSpeedLimit = (limit / 1024);

    if (isAltGlobalSpeedLimitEnabled())
        configureDeferred();
}

int SessionImpl::downloadSpeedLimit() const
{
    return isAltGlobalSpeedLimitEnabled()
            ? altGlobalDownloadSpeedLimit()
            : globalDownloadSpeedLimit();
}

void SessionImpl::setDownloadSpeedLimit(const int limit)
{
    if (isAltGlobalSpeedLimitEnabled())
        setAltGlobalDownloadSpeedLimit(limit);
    else
        setGlobalDownloadSpeedLimit(limit);
}

int SessionImpl::uploadSpeedLimit() const
{
    return isAltGlobalSpeedLimitEnabled()
            ? altGlobalUploadSpeedLimit()
            : globalUploadSpeedLimit();
}

void SessionImpl::setUploadSpeedLimit(const int limit)
{
    if (isAltGlobalSpeedLimitEnabled())
        setAltGlobalUploadSpeedLimit(limit);
    else
        setGlobalUploadSpeedLimit(limit);
}

bool SessionImpl::isAltGlobalSpeedLimitEnabled() const
{
    return m_isAltGlobalSpeedLimitEnabled;
}

void SessionImpl::setAltGlobalSpeedLimitEnabled(const bool enabled)
{
    if (enabled == isAltGlobalSpeedLimitEnabled()) return;

    // Save new state to remember it on startup
    m_isAltGlobalSpeedLimitEnabled = enabled;
    applyBandwidthLimits();
    // Notify
    emit speedLimitModeChanged(m_isAltGlobalSpeedLimitEnabled);
}

bool SessionImpl::isBandwidthSchedulerEnabled() const
{
    return m_isBandwidthSchedulerEnabled;
}

void SessionImpl::setBandwidthSchedulerEnabled(const bool enabled)
{
    if (enabled != isBandwidthSchedulerEnabled())
    {
        m_isBandwidthSchedulerEnabled = enabled;
        if (enabled)
            enableBandwidthScheduler();
        else
            delete m_bwScheduler;
    }
}

bool SessionImpl::isPerformanceWarningEnabled() const
{
    return m_isPerformanceWarningEnabled;
}

void SessionImpl::setPerformanceWarningEnabled(const bool enable)
{
    if (enable == m_isPerformanceWarningEnabled)
        return;

    m_isPerformanceWarningEnabled = enable;
    configureDeferred();
}

int SessionImpl::saveResumeDataInterval() const
{
    return m_saveResumeDataInterval;
}

void SessionImpl::setSaveResumeDataInterval(const int value)
{
    if (value == m_saveResumeDataInterval)
        return;

    m_saveResumeDataInterval = value;

    if (value > 0)
    {
        m_resumeDataTimer->setInterval(std::chrono::minutes(value));
        m_resumeDataTimer->start();
    }
    else
    {
        m_resumeDataTimer->stop();
    }
}

std::chrono::minutes SessionImpl::saveStatisticsInterval() const
{
    return std::chrono::minutes(m_saveStatisticsInterval);
}

void SessionImpl::setSaveStatisticsInterval(const std::chrono::minutes timeInMinutes)
{
    m_saveStatisticsInterval = timeInMinutes.count();
}

int SessionImpl::shutdownTimeout() const
{
    return m_shutdownTimeout;
}

void SessionImpl::setShutdownTimeout(const int value)
{
    m_shutdownTimeout = value;
}

int SessionImpl::port() const
{
    return m_port;
}

void SessionImpl::setPort(const int port)
{
    if (port != m_port)
    {
        m_port = port;
        configureListeningInterface();
    }
}

bool SessionImpl::isSSLEnabled() const
{
    return m_sslEnabled;
}

void SessionImpl::setSSLEnabled(const bool enabled)
{
    if (enabled == isSSLEnabled())
        return;

    m_sslEnabled = enabled;
    configureListeningInterface();
}

int SessionImpl::sslPort() const
{
    return m_sslPort;
}

void SessionImpl::setSSLPort(const int port)
{
    if (port == sslPort())
        return;

    m_sslPort = port;
    configureListeningInterface();
}

QString SessionImpl::networkInterface() const
{
    return m_networkInterface;
}

void SessionImpl::setNetworkInterface(const QString &iface)
{
    if (iface != networkInterface())
    {
        m_networkInterface = iface;
        configureListeningInterface();
    }
}

QString SessionImpl::networkInterfaceName() const
{
    return m_networkInterfaceName;
}

void SessionImpl::setNetworkInterfaceName(const QString &name)
{
    m_networkInterfaceName = name;
}

QString SessionImpl::networkInterfaceAddress() const
{
    return m_networkInterfaceAddress;
}

void SessionImpl::setNetworkInterfaceAddress(const QString &address)
{
    if (address != networkInterfaceAddress())
    {
        m_networkInterfaceAddress = address;
        configureListeningInterface();
    }
}

int SessionImpl::encryption() const
{
    return m_encryption;
}

void SessionImpl::setEncryption(const int state)
{
    if (state != encryption())
    {
        m_encryption = state;
        configureDeferred();
        LogMsg(tr("Encryption support: %1").arg(
            state == 0 ? tr("ON") : ((state == 1) ? tr("FORCED") : tr("OFF")))
            , Log::INFO);
    }
}

int SessionImpl::maxActiveCheckingTorrents() const
{
    return m_maxActiveCheckingTorrents;
}

void SessionImpl::setMaxActiveCheckingTorrents(const int val)
{
    if (val == m_maxActiveCheckingTorrents)
        return;

    m_maxActiveCheckingTorrents = val;
    configureDeferred();
}

bool SessionImpl::isI2PEnabled() const
{
    return m_isI2PEnabled;
}

void SessionImpl::setI2PEnabled(const bool enabled)
{
    if (m_isI2PEnabled != enabled)
    {
        m_isI2PEnabled = enabled;
        configureDeferred();
    }
}

QString SessionImpl::I2PAddress() const
{
    return m_I2PAddress;
}

void SessionImpl::setI2PAddress(const QString &address)
{
    if (m_I2PAddress != address)
    {
        m_I2PAddress = address;
        configureDeferred();
    }
}

int SessionImpl::I2PPort() const
{
    return m_I2PPort;
}

void SessionImpl::setI2PPort(int port)
{
    if (m_I2PPort != port)
    {
        m_I2PPort = port;
        configureDeferred();
    }
}

bool SessionImpl::I2PMixedMode() const
{
    return m_I2PMixedMode;
}

void SessionImpl::setI2PMixedMode(const bool enabled)
{
    if (m_I2PMixedMode != enabled)
    {
        m_I2PMixedMode = enabled;
        configureDeferred();
    }
}

int SessionImpl::I2PInboundQuantity() const
{
    return m_I2PInboundQuantity;
}

void SessionImpl::setI2PInboundQuantity(const int value)
{
    if (value == m_I2PInboundQuantity)
        return;

    m_I2PInboundQuantity = value;
    configureDeferred();
}

int SessionImpl::I2POutboundQuantity() const
{
    return m_I2POutboundQuantity;
}

void SessionImpl::setI2POutboundQuantity(const int value)
{
    if (value == m_I2POutboundQuantity)
        return;

    m_I2POutboundQuantity = value;
    configureDeferred();
}

int SessionImpl::I2PInboundLength() const
{
    return m_I2PInboundLength;
}

void SessionImpl::setI2PInboundLength(const int value)
{
    if (value == m_I2PInboundLength)
        return;

    m_I2PInboundLength = value;
    configureDeferred();
}

int SessionImpl::I2POutboundLength() const
{
    return m_I2POutboundLength;
}

void SessionImpl::setI2POutboundLength(const int value)
{
    if (value == m_I2POutboundLength)
        return;

    m_I2POutboundLength = value;
    configureDeferred();
}

bool SessionImpl::isProxyPeerConnectionsEnabled() const
{
    return m_isProxyPeerConnectionsEnabled;
}

void SessionImpl::setProxyPeerConnectionsEnabled(const bool enabled)
{
    if (enabled != isProxyPeerConnectionsEnabled())
    {
        m_isProxyPeerConnectionsEnabled = enabled;
        configureDeferred();
    }
}

ChokingAlgorithm SessionImpl::chokingAlgorithm() const
{
    return m_chokingAlgorithm;
}

void SessionImpl::setChokingAlgorithm(const ChokingAlgorithm mode)
{
    if (mode == m_chokingAlgorithm) return;

    m_chokingAlgorithm = mode;
    configureDeferred();
}

SeedChokingAlgorithm SessionImpl::seedChokingAlgorithm() const
{
    return m_seedChokingAlgorithm;
}

void SessionImpl::setSeedChokingAlgorithm(const SeedChokingAlgorithm mode)
{
    if (mode == m_seedChokingAlgorithm) return;

    m_seedChokingAlgorithm = mode;
    configureDeferred();
}

bool SessionImpl::isAddTrackersEnabled() const
{
    return m_isAddTrackersEnabled;
}

void SessionImpl::setAddTrackersEnabled(const bool enabled)
{
    m_isAddTrackersEnabled = enabled;
}

QString SessionImpl::additionalTrackers() const
{
    return m_additionalTrackers;
}

void SessionImpl::setAdditionalTrackers(const QString &trackers)
{
    if (trackers == additionalTrackers())
        return;

    m_additionalTrackers = trackers;
    populateAdditionalTrackers();
}

bool SessionImpl::isAddTrackersFromURLEnabled() const
{
    return m_isAddTrackersFromURLEnabled;
}

void SessionImpl::setAddTrackersFromURLEnabled(const bool enabled)
{
    if (enabled != isAddTrackersFromURLEnabled())
    {
        m_isAddTrackersFromURLEnabled = enabled;
        if (enabled)
        {
            updateTrackersFromURL();
            m_updateTrackersFromURLTimer->start();
        }
        else
        {
            m_updateTrackersFromURLTimer->stop();
            setAdditionalTrackersFromURL({});
            const Path path = specialFolderLocation(SpecialFolder::Data) / Path(ADDITIONAL_TRACKERS_FROM_URL_FILE_NAME);
            Utils::Fs::removeFile(path);
        }
    }
}

QString SessionImpl::additionalTrackersURL() const
{
    return m_additionalTrackersURL;
}

void SessionImpl::setAdditionalTrackersURL(const QString &url)
{
    if (url != additionalTrackersURL())
    {
        m_additionalTrackersURL = url.trimmed();
        if (isAddTrackersFromURLEnabled())
            updateTrackersFromURL();
    }
}

QString SessionImpl::additionalTrackersFromURL() const
{
    return m_additionalTrackersFromURL;
}

void SessionImpl::setAdditionalTrackersFromURL(const QString &trackers)
{
    if (trackers != additionalTrackersFromURL())
    {
        m_additionalTrackersFromURL = trackers;
        populateAdditionalTrackersFromURL();
    }
}

void SessionImpl::updateTrackersFromURL()
{
    const QString url = additionalTrackersURL();
    if (url.isEmpty())
    {
        setAdditionalTrackersFromURL({});
    }
    else
    {
        const Path path = specialFolderLocation(SpecialFolder::Data) / ADDITIONAL_TRACKERS_FROM_URL_FILE_NAME;
        Net::DownloadManager::instance()->download(Net::DownloadRequest(url).saveToFile(true).destFileName(path)
                , Preferences::instance()->useProxyForGeneralPurposes(), this, [this](const Net::DownloadResult &result)
        {
            if (result.status == Net::DownloadStatus::Success)
            {
                setAdditionalTrackersFromURL(QString::fromUtf8(result.data));
                LogMsg(tr("Tracker list updated"), Log::INFO);
                return;
            }

            LogMsg(tr("Failed to update tracker list. Reason: \"%1\"").arg(result.errorString), Log::WARNING);
        });
    }
}

bool SessionImpl::isIPFilteringEnabled() const
{
    return m_isIPFilteringEnabled;
}

void SessionImpl::setIPFilteringEnabled(const bool enabled)
{
    if (enabled != m_isIPFilteringEnabled)
    {
        m_isIPFilteringEnabled = enabled;
        m_IPFilteringConfigured = false;
        configureDeferred();
    }
}

Path SessionImpl::IPFilterFile() const
{
    return m_IPFilterFile;
}

void SessionImpl::setIPFilterFile(const Path &path)
{
    if (path != IPFilterFile())
    {
        m_IPFilterFile = path;
        m_IPFilteringConfigured = false;
        configureDeferred();
    }
}

bool SessionImpl::isExcludedFileNamesEnabled() const
{
    return m_isExcludedFileNamesEnabled;
}

void SessionImpl::setExcludedFileNamesEnabled(const bool enabled)
{
    if (m_isExcludedFileNamesEnabled == enabled)
        return;

    m_isExcludedFileNamesEnabled = enabled;

    if (enabled)
        populateExcludedFileNamesRegExpList();
    else
        m_excludedFileNamesRegExpList.clear();
}

QStringList SessionImpl::excludedFileNames() const
{
    return m_excludedFileNames;
}

void SessionImpl::setExcludedFileNames(const QStringList &excludedFileNames)
{
    if (excludedFileNames != m_excludedFileNames)
    {
        m_excludedFileNames = excludedFileNames;
        populateExcludedFileNamesRegExpList();
    }
}

void SessionImpl::populateExcludedFileNamesRegExpList()
{
    const QStringList excludedNames = excludedFileNames();

    m_excludedFileNamesRegExpList.clear();
    m_excludedFileNamesRegExpList.reserve(excludedNames.size());

    for (const QString &str : excludedNames)
    {
        const QString pattern = QRegularExpression::wildcardToRegularExpression(str);
        const QRegularExpression re {pattern, QRegularExpression::CaseInsensitiveOption};
        m_excludedFileNamesRegExpList.append(re);
    }
}

void SessionImpl::applyFilenameFilter(const PathList &files, QList<DownloadPriority> &priorities)
{
    if (!isExcludedFileNamesEnabled())
        return;

    const auto isFilenameExcluded = [patterns = m_excludedFileNamesRegExpList](const Path &fileName)
    {
        return std::ranges::any_of(patterns, [&fileName](const QRegularExpression &re)
        {
            Path path = fileName;
            while (!re.match(path.filename()).hasMatch())
            {
                path = path.parentPath();
                if (path.isEmpty())
                    return false;
            }
            return true;
        });
    };

    priorities.resize(files.count(), DownloadPriority::Normal);
    for (qsizetype i = 0; i < priorities.size(); ++i)
    {
        if (priorities[i] == BitTorrent::DownloadPriority::Ignored)
            continue;

        if (isFilenameExcluded(files.at(i)))
            priorities[i] = BitTorrent::DownloadPriority::Ignored;
    }
}

void SessionImpl::setBannedIPs(const QStringList &newList)
{
    if (newList == m_bannedIPs)
        return; // do nothing
    // here filter out incorrect IP
    QStringList filteredList;
    for (const QString &ip : newList)
    {
        if (Utils::Net::isValidIP(ip))
        {
            // the same IPv6 addresses could be written in different forms;
            // QHostAddress::toString() result format follows RFC5952;
            // thus we avoid duplicate entries pointing to the same address
            filteredList << QHostAddress(ip).toString();
        }
        else
        {
            LogMsg(tr("Rejected invalid IP address while applying the list of banned IP addresses. IP: \"%1\"")
                   .arg(ip)
                , Log::WARNING);
        }
    }
    // now we have to sort IPs and make them unique
    filteredList.sort();
    filteredList.removeDuplicates();
    // Again ensure that the new list is different from the stored one.
    if (filteredList == m_bannedIPs)
        return; // do nothing
    // store to session settings
    // also here we have to recreate filter list including 3rd party ban file
    // and install it again into m_session
    m_bannedIPs = filteredList;
    m_IPFilteringConfigured = false;
    configureDeferred();
}

ResumeDataStorageType SessionImpl::resumeDataStorageType() const
{
    return m_resumeDataStorageType;
}

void SessionImpl::setResumeDataStorageType(const ResumeDataStorageType type)
{
    m_resumeDataStorageType = type;
}

bool SessionImpl::isMergeTrackersEnabled() const
{
    return m_isMergeTrackersEnabled;
}

void SessionImpl::setMergeTrackersEnabled(const bool enabled)
{
    m_isMergeTrackersEnabled = enabled;
}

bool SessionImpl::isStartPaused() const
{
    return m_startPaused.get(false);
}

void SessionImpl::setStartPaused(const bool value)
{
    m_startPaused = value;
}

TorrentContentRemoveOption SessionImpl::torrentContentRemoveOption() const
{
    return m_torrentContentRemoveOption;
}

void SessionImpl::setTorrentContentRemoveOption(const TorrentContentRemoveOption option)
{
    m_torrentContentRemoveOption = option;
}

QStringList SessionImpl::bannedIPs() const
{
    return m_bannedIPs;
}

bool SessionImpl::isRestored() const
{
    return m_isRestored;
}

bool SessionImpl::isPaused() const
{
    return m_isPaused;
}

void SessionImpl::pause()
{
    if (m_isPaused)
        return;

    if (isRestored())
    {
        m_nativeSession->pause();

        for (TorrentImpl *torrent : asConst(m_torrents))
        {
            torrent->resetTrackerEntryStatuses();

            const QList<TrackerEntryStatus> trackers = torrent->trackers();
            QHash<QString, TrackerEntryStatus> updatedTrackers;
            updatedTrackers.reserve(trackers.size());

            for (const TrackerEntryStatus &status : trackers)
                updatedTrackers.emplace(status.url, status);
            emit trackerEntryStatusesUpdated(torrent, updatedTrackers);
        }
    }

    m_isPaused = true;
    emit paused();
}

void SessionImpl::resume()
{
    if (m_isPaused)
    {
        if (isRestored())
            m_nativeSession->resume();

        m_isPaused = false;
        emit resumed();
    }
}

int SessionImpl::maxConnectionsPerTorrent() const
{
    return m_maxConnectionsPerTorrent;
}

void SessionImpl::setMaxConnectionsPerTorrent(int max)
{
    max = (max > 0) ? max : -1;
    if (max != maxConnectionsPerTorrent())
    {
        m_maxConnectionsPerTorrent = max;

        for (const TorrentImpl *torrent : asConst(m_torrents))
        {
            try
            {
                torrent->nativeHandle().set_max_connections(max);
            }
            catch (const std::exception &) {}
        }
    }
}

int SessionImpl::maxUploadsPerTorrent() const
{
    return m_maxUploadsPerTorrent;
}

void SessionImpl::setMaxUploadsPerTorrent(int max)
{
    max = (max > 0) ? max : -1;
    if (max != maxUploadsPerTorrent())
    {
        m_maxUploadsPerTorrent = max;

        for (const TorrentImpl *torrent : asConst(m_torrents))
        {
            try
            {
                torrent->nativeHandle().set_max_uploads(max);
            }
            catch (const std::exception &) {}
        }
    }
}

bool SessionImpl::announceToAllTrackers() const
{
    return m_announceToAllTrackers;
}

void SessionImpl::setAnnounceToAllTrackers(const bool val)
{
    if (val != m_announceToAllTrackers)
    {
        m_announceToAllTrackers = val;
        configureDeferred();
    }
}

bool SessionImpl::announceToAllTiers() const
{
    return m_announceToAllTiers;
}

void SessionImpl::setAnnounceToAllTiers(const bool val)
{
    if (val != m_announceToAllTiers)
    {
        m_announceToAllTiers = val;
        configureDeferred();
    }
}

int SessionImpl::peerTurnover() const
{
    return m_peerTurnover;
}

void SessionImpl::setPeerTurnover(const int val)
{
    if (val == m_peerTurnover)
        return;

    m_peerTurnover = val;
    configureDeferred();
}

int SessionImpl::peerTurnoverCutoff() const
{
    return m_peerTurnoverCutoff;
}

void SessionImpl::setPeerTurnoverCutoff(const int val)
{
    if (val == m_peerTurnoverCutoff)
        return;

    m_peerTurnoverCutoff = val;
    configureDeferred();
}

int SessionImpl::peerTurnoverInterval() const
{
    return m_peerTurnoverInterval;
}

void SessionImpl::setPeerTurnoverInterval(const int val)
{
    if (val == m_peerTurnoverInterval)
        return;

    m_peerTurnoverInterval = val;
    configureDeferred();
}

DiskIOType SessionImpl::diskIOType() const
{
    return m_diskIOType;
}

void SessionImpl::setDiskIOType(const DiskIOType type)
{
    if (type != m_diskIOType)
    {
        m_diskIOType = type;
    }
}

int SessionImpl::requestQueueSize() const
{
    return m_requestQueueSize;
}

void SessionImpl::setRequestQueueSize(const int val)
{
    if (val == m_requestQueueSize)
        return;

    m_requestQueueSize = val;
    configureDeferred();
}

int SessionImpl::asyncIOThreads() const
{
    return std::clamp(m_asyncIOThreads.get(), 1, 1024);
}

void SessionImpl::setAsyncIOThreads(const int num)
{
    if (num == m_asyncIOThreads)
        return;

    m_asyncIOThreads = num;
    configureDeferred();
}

int SessionImpl::hashingThreads() const
{
    return std::clamp(m_hashingThreads.get(), 1, 1024);
}

void SessionImpl::setHashingThreads(const int num)
{
    if (num == m_hashingThreads)
        return;

    m_hashingThreads = num;
    configureDeferred();
}

int SessionImpl::filePoolSize() const
{
    return m_filePoolSize;
}

void SessionImpl::setFilePoolSize(const int size)
{
    if (size == m_filePoolSize)
        return;

    m_filePoolSize = size;
    configureDeferred();
}

int SessionImpl::checkingMemUsage() const
{
    return std::max(1, m_checkingMemUsage.get());
}

void SessionImpl::setCheckingMemUsage(int size)
{
    size = std::max(size, 1);

    if (size == m_checkingMemUsage)
        return;

    m_checkingMemUsage = size;
    configureDeferred();
}

int SessionImpl::diskCacheSize() const
{
#ifdef QBT_APP_64BIT
    return std::min(m_diskCacheSize.get(), 33554431);  // 32768GiB
#else
    // When build as 32bit binary, set the maximum at less than 2GB to prevent crashes
    // allocate 1536MiB and leave 512MiB to the rest of program data in RAM
    return std::min(m_diskCacheSize.get(), 1536);
#endif
}

void SessionImpl::setDiskCacheSize(int size)
{
#ifdef QBT_APP_64BIT
    size = std::min(size, 33554431);  // 32768GiB
#else
    // allocate 1536MiB and leave 512MiB to the rest of program data in RAM
    size = std::min(size, 1536);
#endif
    if (size != m_diskCacheSize)
    {
        m_diskCacheSize = size;
        configureDeferred();
    }
}

int SessionImpl::diskCacheTTL() const
{
    return m_diskCacheTTL;
}

void SessionImpl::setDiskCacheTTL(const int ttl)
{
    if (ttl != m_diskCacheTTL)
    {
        m_diskCacheTTL = ttl;
        configureDeferred();
    }
}

qint64 SessionImpl::diskQueueSize() const
{
    return m_diskQueueSize;
}

void SessionImpl::setDiskQueueSize(const qint64 size)
{
    if (size == m_diskQueueSize)
        return;

    m_diskQueueSize = size;
    configureDeferred();
}

DiskIOReadMode SessionImpl::diskIOReadMode() const
{
    return m_diskIOReadMode;
}

void SessionImpl::setDiskIOReadMode(const DiskIOReadMode mode)
{
    if (mode == m_diskIOReadMode)
        return;

    m_diskIOReadMode = mode;
    configureDeferred();
}

DiskIOWriteMode SessionImpl::diskIOWriteMode() const
{
    return m_diskIOWriteMode;
}

void SessionImpl::setDiskIOWriteMode(const DiskIOWriteMode mode)
{
    if (mode == m_diskIOWriteMode)
        return;

    m_diskIOWriteMode = mode;
    configureDeferred();
}

bool SessionImpl::isCoalesceReadWriteEnabled() const
{
    return m_coalesceReadWriteEnabled;
}

void SessionImpl::setCoalesceReadWriteEnabled(const bool enabled)
{
    if (enabled == m_coalesceReadWriteEnabled) return;

    m_coalesceReadWriteEnabled = enabled;
    configureDeferred();
}

bool SessionImpl::isSuggestModeEnabled() const
{
    return m_isSuggestMode;
}

bool SessionImpl::usePieceExtentAffinity() const
{
    return m_usePieceExtentAffinity;
}

void SessionImpl::setPieceExtentAffinity(const bool enabled)
{
    if (enabled == m_usePieceExtentAffinity) return;

    m_usePieceExtentAffinity = enabled;
    configureDeferred();
}

void SessionImpl::setSuggestMode(const bool mode)
{
    if (mode == m_isSuggestMode) return;

    m_isSuggestMode = mode;
    configureDeferred();
}

int SessionImpl::sendBufferWatermark() const
{
    return m_sendBufferWatermark;
}

void SessionImpl::setSendBufferWatermark(const int value)
{
    if (value == m_sendBufferWatermark) return;

    m_sendBufferWatermark = value;
    configureDeferred();
}

int SessionImpl::sendBufferLowWatermark() const
{
    return m_sendBufferLowWatermark;
}

void SessionImpl::setSendBufferLowWatermark(const int value)
{
    if (value == m_sendBufferLowWatermark) return;

    m_sendBufferLowWatermark = value;
    configureDeferred();
}

int SessionImpl::sendBufferWatermarkFactor() const
{
    return m_sendBufferWatermarkFactor;
}

void SessionImpl::setSendBufferWatermarkFactor(const int value)
{
    if (value == m_sendBufferWatermarkFactor) return;

    m_sendBufferWatermarkFactor = value;
    configureDeferred();
}

int SessionImpl::connectionSpeed() const
{
    return m_connectionSpeed;
}

void SessionImpl::setConnectionSpeed(const int value)
{
    if (value == m_connectionSpeed) return;

    m_connectionSpeed = value;
    configureDeferred();
}

int SessionImpl::socketSendBufferSize() const
{
    return m_socketSendBufferSize;
}

void SessionImpl::setSocketSendBufferSize(const int value)
{
    if (value == m_socketSendBufferSize)
        return;

    m_socketSendBufferSize = value;
    configureDeferred();
}

int SessionImpl::socketReceiveBufferSize() const
{
    return m_socketReceiveBufferSize;
}

void SessionImpl::setSocketReceiveBufferSize(const int value)
{
    if (value == m_socketReceiveBufferSize)
        return;

    m_socketReceiveBufferSize = value;
    configureDeferred();
}

int SessionImpl::socketBacklogSize() const
{
    return m_socketBacklogSize;
}

void SessionImpl::setSocketBacklogSize(const int value)
{
    if (value == m_socketBacklogSize) return;

    m_socketBacklogSize = value;
    configureDeferred();
}

bool SessionImpl::isAnonymousModeEnabled() const
{
    return m_isAnonymousModeEnabled;
}

void SessionImpl::setAnonymousModeEnabled(const bool enabled)
{
    if (enabled != m_isAnonymousModeEnabled)
    {
        m_isAnonymousModeEnabled = enabled;
        configureDeferred();
        LogMsg(tr("Anonymous mode: %1").arg(isAnonymousModeEnabled() ? tr("ON") : tr("OFF"))
            , Log::INFO);
    }
}

bool SessionImpl::isQueueingSystemEnabled() const
{
    return m_isQueueingEnabled;
}

void SessionImpl::setQueueingSystemEnabled(const bool enabled)
{
    if (enabled != m_isQueueingEnabled)
    {
        m_isQueueingEnabled = enabled;
        configureDeferred();

        if (enabled)
            m_torrentsQueueChanged = true;
        else
            removeTorrentsQueue();

        for (TorrentImpl *torrent : asConst(m_torrents))
            torrent->handleQueueingModeChanged();
    }
}

int SessionImpl::maxActiveDownloads() const
{
    return m_maxActiveDownloads;
}

void SessionImpl::setMaxActiveDownloads(int max)
{
    max = std::max(max, -1);
    if (max != m_maxActiveDownloads)
    {
        m_maxActiveDownloads = max;
        configureDeferred();
    }
}

int SessionImpl::maxActiveUploads() const
{
    return m_maxActiveUploads;
}

void SessionImpl::setMaxActiveUploads(int max)
{
    max = std::max(max, -1);
    if (max != m_maxActiveUploads)
    {
        m_maxActiveUploads = max;
        configureDeferred();
    }
}

int SessionImpl::maxActiveTorrents() const
{
    return m_maxActiveTorrents;
}

void SessionImpl::setMaxActiveTorrents(int max)
{
    max = std::max(max, -1);
    if (max != m_maxActiveTorrents)
    {
        m_maxActiveTorrents = max;
        configureDeferred();
    }
}

bool SessionImpl::ignoreSlowTorrentsForQueueing() const
{
    return m_ignoreSlowTorrentsForQueueing;
}

void SessionImpl::setIgnoreSlowTorrentsForQueueing(const bool ignore)
{
    if (ignore != m_ignoreSlowTorrentsForQueueing)
    {
        m_ignoreSlowTorrentsForQueueing = ignore;
        configureDeferred();
    }
}

int SessionImpl::downloadRateForSlowTorrents() const
{
    return m_downloadRateForSlowTorrents;
}

void SessionImpl::setDownloadRateForSlowTorrents(const int rateInKibiBytes)
{
    if (rateInKibiBytes == m_downloadRateForSlowTorrents)
        return;

    m_downloadRateForSlowTorrents = rateInKibiBytes;
    configureDeferred();
}

int SessionImpl::uploadRateForSlowTorrents() const
{
    return m_uploadRateForSlowTorrents;
}

void SessionImpl::setUploadRateForSlowTorrents(const int rateInKibiBytes)
{
    if (rateInKibiBytes == m_uploadRateForSlowTorrents)
        return;

    m_uploadRateForSlowTorrents = rateInKibiBytes;
    configureDeferred();
}

int SessionImpl::slowTorrentsInactivityTimer() const
{
    return m_slowTorrentsInactivityTimer;
}

void SessionImpl::setSlowTorrentsInactivityTimer(const int timeInSeconds)
{
    if (timeInSeconds == m_slowTorrentsInactivityTimer)
        return;

    m_slowTorrentsInactivityTimer = timeInSeconds;
    configureDeferred();
}

int SessionImpl::outgoingPortsMin() const
{
    return m_outgoingPortsMin;
}

void SessionImpl::setOutgoingPortsMin(const int min)
{
    if (min != m_outgoingPortsMin)
    {
        m_outgoingPortsMin = min;
        configureDeferred();
    }
}

int SessionImpl::outgoingPortsMax() const
{
    return m_outgoingPortsMax;
}

void SessionImpl::setOutgoingPortsMax(const int max)
{
    if (max != m_outgoingPortsMax)
    {
        m_outgoingPortsMax = max;
        configureDeferred();
    }
}

int SessionImpl::UPnPLeaseDuration() const
{
    return m_UPnPLeaseDuration;
}

void SessionImpl::setUPnPLeaseDuration(const int duration)
{
    if (duration != m_UPnPLeaseDuration)
    {
        m_UPnPLeaseDuration = duration;
        configureDeferred();
    }
}

int SessionImpl::peerDSCP() const
{
    return m_peerDSCP;
}

void SessionImpl::setPeerDSCP(const int value)
{
    if (value == m_peerDSCP)
        return;

    m_peerDSCP = value;
    configureDeferred();
}

bool SessionImpl::ignoreLimitsOnLAN() const
{
    return m_ignoreLimitsOnLAN;
}

void SessionImpl::setIgnoreLimitsOnLAN(const bool ignore)
{
    if (ignore != m_ignoreLimitsOnLAN)
    {
        m_ignoreLimitsOnLAN = ignore;
        configureDeferred();
    }
}

bool SessionImpl::includeOverheadInLimits() const
{
    return m_includeOverheadInLimits;
}

void SessionImpl::setIncludeOverheadInLimits(const bool include)
{
    if (include != m_includeOverheadInLimits)
    {
        m_includeOverheadInLimits = include;
        configureDeferred();
    }
}

QString SessionImpl::announceIP() const
{
    return m_announceIP;
}

void SessionImpl::setAnnounceIP(const QString &ip)
{
    if (ip != m_announceIP)
    {
        m_announceIP = ip;
        configureDeferred();
    }
}

int SessionImpl::announcePort() const
{
    return m_announcePort;
}

void SessionImpl::setAnnouncePort(const int port)
{
    if (port != m_announcePort)
    {
        m_announcePort = port;
        configureDeferred();
    }
}

int SessionImpl::maxConcurrentHTTPAnnounces() const
{
    return m_maxConcurrentHTTPAnnounces;
}

void SessionImpl::setMaxConcurrentHTTPAnnounces(const int value)
{
    if (value == m_maxConcurrentHTTPAnnounces)
        return;

    m_maxConcurrentHTTPAnnounces = value;
    configureDeferred();
}

bool SessionImpl::isReannounceWhenAddressChangedEnabled() const
{
    return m_isReannounceWhenAddressChangedEnabled;
}

void SessionImpl::setReannounceWhenAddressChangedEnabled(const bool enabled)
{
    if (enabled == m_isReannounceWhenAddressChangedEnabled)
        return;

    m_isReannounceWhenAddressChangedEnabled = enabled;
}

void SessionImpl::reannounceToAllTrackers() const
{
    for (const TorrentImpl *torrent : asConst(m_torrents))
    {
        try
        {
            torrent->nativeHandle().force_reannounce(0, -1, lt::torrent_handle::ignore_min_interval);
        }
        catch (const std::exception &) {}
    }
}

int SessionImpl::stopTrackerTimeout() const
{
    return m_stopTrackerTimeout;
}

void SessionImpl::setStopTrackerTimeout(const int value)
{
    if (value == m_stopTrackerTimeout)
        return;

    m_stopTrackerTimeout = value;
    configureDeferred();
}

int SessionImpl::maxConnections() const
{
    return m_maxConnections;
}

void SessionImpl::setMaxConnections(int max)
{
    max = (max > 0) ? max : -1;
    if (max != m_maxConnections)
    {
        m_maxConnections = max;
        configureDeferred();
    }
}

int SessionImpl::maxUploads() const
{
    return m_maxUploads;
}

void SessionImpl::setMaxUploads(int max)
{
    max = (max > 0) ? max : -1;
    if (max != m_maxUploads)
    {
        m_maxUploads = max;
        configureDeferred();
    }
}

BTProtocol SessionImpl::btProtocol() const
{
    return m_btProtocol;
}

void SessionImpl::setBTProtocol(const BTProtocol protocol)
{
    if ((protocol < BTProtocol::Both) || (BTProtocol::UTP < protocol))
        return;

    if (protocol == m_btProtocol) return;

    m_btProtocol = protocol;
    configureDeferred();
}

bool SessionImpl::isUTPRateLimited() const
{
    return m_isUTPRateLimited;
}

void SessionImpl::setUTPRateLimited(const bool limited)
{
    if (limited != m_isUTPRateLimited)
    {
        m_isUTPRateLimited = limited;
        configureDeferred();
    }
}

MixedModeAlgorithm SessionImpl::utpMixedMode() const
{
    return m_utpMixedMode;
}

void SessionImpl::setUtpMixedMode(const MixedModeAlgorithm mode)
{
    if (mode == m_utpMixedMode) return;

    m_utpMixedMode = mode;
    configureDeferred();
}

int SessionImpl::hostnameCacheTTL() const
{
    return m_hostnameCacheTTL;
}

void SessionImpl::setHostnameCacheTTL(const int value)
{
    if (value == hostnameCacheTTL())
        return;

    m_hostnameCacheTTL = value;
    configureDeferred();
}

bool SessionImpl::isIDNSupportEnabled() const
{
    return m_IDNSupportEnabled;
}

void SessionImpl::setIDNSupportEnabled(const bool enabled)
{
    if (enabled == m_IDNSupportEnabled) return;

    m_IDNSupportEnabled = enabled;
    configureDeferred();
}

bool SessionImpl::multiConnectionsPerIpEnabled() const
{
    return m_multiConnectionsPerIpEnabled;
}

void SessionImpl::setMultiConnectionsPerIpEnabled(const bool enabled)
{
    if (enabled == m_multiConnectionsPerIpEnabled) return;

    m_multiConnectionsPerIpEnabled = enabled;
    configureDeferred();
}

bool SessionImpl::validateHTTPSTrackerCertificate() const
{
    return m_validateHTTPSTrackerCertificate;
}

void SessionImpl::setValidateHTTPSTrackerCertificate(const bool enabled)
{
    if (enabled == m_validateHTTPSTrackerCertificate) return;

    m_validateHTTPSTrackerCertificate = enabled;
    configureDeferred();
}

bool SessionImpl::isSSRFMitigationEnabled() const
{
    return m_SSRFMitigationEnabled;
}

void SessionImpl::setSSRFMitigationEnabled(const bool enabled)
{
    if (enabled == m_SSRFMitigationEnabled) return;

    m_SSRFMitigationEnabled = enabled;
    configureDeferred();
}

bool SessionImpl::blockPeersOnPrivilegedPorts() const
{
    return m_blockPeersOnPrivilegedPorts;
}

void SessionImpl::setBlockPeersOnPrivilegedPorts(const bool enabled)
{
    if (enabled == m_blockPeersOnPrivilegedPorts) return;

    m_blockPeersOnPrivilegedPorts = enabled;
    configureDeferred();
}

bool SessionImpl::isTrackerFilteringEnabled() const
{
    return m_isTrackerFilteringEnabled;
}

void SessionImpl::setTrackerFilteringEnabled(const bool enabled)
{
    if (enabled != m_isTrackerFilteringEnabled)
    {
        m_isTrackerFilteringEnabled = enabled;
        configureDeferred();
    }
}

bool SessionImpl::isAutoBanUnknownPeerEnabled() const
{
    return m_autoBanUnknownPeer;
}

void SessionImpl::setAutoBanUnknownPeer(bool value)
{
    if (value != isAutoBanUnknownPeerEnabled()) {
        m_autoBanUnknownPeer = value;
        LogMsg(tr("Restart is required to toggle Auto Ban Unknown Client support"), Log::WARNING);
    }
}

bool SessionImpl::isAutoBanBTPlayerPeerEnabled() const
{
    return m_autoBanBTPlayerPeer;
}

void SessionImpl::setAutoBanBTPlayerPeer(bool value)
{
    if (value != isAutoBanBTPlayerPeerEnabled()) {
        m_autoBanBTPlayerPeer = value;
        LogMsg(tr("Restart is required to toggle Auto Ban Bittorrent Media Player support"), Log::WARNING);
    }
}

bool SessionImpl::isShadowBanEnabled() const
{
    return m_shadowBan;
}

void SessionImpl::setShadowBan(bool value)
{
    if (value != isShadowBanEnabled()) {
        m_shadowBan = value;
        LogMsg(tr("Restart is required to toggle Auto Ban Shadow Ban"), Log::WARNING);
    }
}

QStringList SessionImpl::shadowBannedIPs() const
{
    return m_shadowBannedIPs;
}

void SessionImpl::setShadowBannedIPs(const QStringList &newList)
{
    if (newList == m_shadowBannedIPs)
        return; // do nothing
    // here filter out incorrect IP
    QStringList filteredList;
    for (const QString &ip : newList)
    {
        if (Utils::Net::isValidIP(ip))
        {
            // the same IPv6 addresses could be written in different forms;
            // QHostAddress::toString() result format follows RFC5952;
            // thus we avoid duplicate entries pointing to the same address
            filteredList << QHostAddress(ip).toString();
        }
        else
        {
            LogMsg(tr("Rejected invalid IP address while applying the list of shadow banned IP addresses. IP: \"%1\"")
                   .arg(ip)
                , Log::WARNING);
        }
    }
    // now we have to sort IPs and make them unique
    filteredList.sort();
    filteredList.removeDuplicates();
    // Again ensure that the new list is different from the stored one.
    if (filteredList == m_shadowBannedIPs)
        return; // do nothing
    // store to session settings
    // also here we have to recreate filter list including 3rd party ban file
    // and install it again into m_session
    m_shadowBannedIPs = filteredList;
    configureDeferred();
}

QString SessionImpl::lastExternalIPv4Address() const
{
    return m_lastExternalIPv4Address;
}

QString SessionImpl::lastExternalIPv6Address() const
{
    return m_lastExternalIPv6Address;
}

qint64 SessionImpl::freeDiskSpace() const
{
    return m_freeDiskSpace;
}

bool SessionImpl::isListening() const
{
    return m_nativeSessionExtension->isSessionListening();
}

ShareLimitAction SessionImpl::shareLimitAction() const
{
    return m_shareLimitAction;
}

void SessionImpl::setShareLimitAction(const ShareLimitAction act)
{
    Q_ASSERT(act != ShareLimitAction::Default);

    m_shareLimitAction = act;
}

bool SessionImpl::isKnownTorrent(const InfoHash &infoHash) const
{
    const bool isHybrid = infoHash.isHybrid();
    const auto id = TorrentID::fromInfoHash(infoHash);
    // alternative ID can be useful to find existing torrent
    // in case if hybrid torrent was added by v1 info hash
    const auto altID = (isHybrid ? TorrentID::fromSHA1Hash(infoHash.v1()) : TorrentID());

    if (m_downloadedMetadata.contains(id) || (isHybrid && m_downloadedMetadata.contains(altID)))
        return true;
    return findTorrent(infoHash);
}

void SessionImpl::updateSeedingLimitTimer()
{
    if ((globalMaxRatio() == NO_RATIO_LIMIT) && !hasPerTorrentRatioLimit()
        && (globalMaxSeedingMinutes() == NO_SEEDING_TIME_LIMIT) && !hasPerTorrentSeedingTimeLimit()
        && (globalMaxInactiveSeedingMinutes() == NO_SEEDING_TIME_LIMIT) && !hasPerTorrentInactiveSeedingTimeLimit())
    {
        if (m_seedingLimitTimer->isActive())
            m_seedingLimitTimer->stop();
    }
    else if (!m_seedingLimitTimer->isActive())
    {
        m_seedingLimitTimer->start();
    }
}

void SessionImpl::handleTorrentShareLimitChanged(TorrentImpl *const)
{
    updateSeedingLimitTimer();
}

void SessionImpl::handleTorrentNameChanged(TorrentImpl *const)
{
}

void SessionImpl::handleTorrentSavePathChanged(TorrentImpl *const torrent)
{
    emit torrentSavePathChanged(torrent);
}

void SessionImpl::handleTorrentCategoryChanged(TorrentImpl *const torrent, const QString &oldCategory)
{
    emit torrentCategoryChanged(torrent, oldCategory);
}

void SessionImpl::handleTorrentTagAdded(TorrentImpl *const torrent, const Tag &tag)
{
    emit torrentTagAdded(torrent, tag);
}

void SessionImpl::handleTorrentTagRemoved(TorrentImpl *const torrent, const Tag &tag)
{
    emit torrentTagRemoved(torrent, tag);
}

void SessionImpl::handleTorrentSavingModeChanged(TorrentImpl *const torrent)
{
    emit torrentSavingModeChanged(torrent);
}

void SessionImpl::handleTorrentTrackersAdded(TorrentImpl *const torrent, const QList<TrackerEntry> &newTrackers)
{
    for (const TrackerEntry &newTracker : newTrackers)
        LogMsg(tr("Added tracker to torrent. Torrent: \"%1\". Tracker: \"%2\"").arg(torrent->name(), newTracker.url));
    emit trackersAdded(torrent, newTrackers);
}

void SessionImpl::handleTorrentTrackersRemoved(TorrentImpl *const torrent, const QStringList &deletedTrackers)
{
    for (const QString &deletedTracker : deletedTrackers)
        LogMsg(tr("Removed tracker from torrent. Torrent: \"%1\". Tracker: \"%2\"").arg(torrent->name(), deletedTracker));
    emit trackersRemoved(torrent, deletedTrackers);
}

void SessionImpl::handleTorrentTrackersReset(TorrentImpl *const torrent, const QList<TrackerEntryStatus> &oldEntries, const QList<TrackerEntry> &newEntries)
{
    emit trackersReset(torrent, oldEntries, newEntries);
}

void SessionImpl::handleTorrentUrlSeedsAdded(TorrentImpl *const torrent, const QList<QUrl> &newUrlSeeds)
{
    for (const QUrl &newUrlSeed : newUrlSeeds)
        LogMsg(tr("Added URL seed to torrent. Torrent: \"%1\". URL: \"%2\"").arg(torrent->name(), newUrlSeed.toString()));
}

void SessionImpl::handleTorrentUrlSeedsRemoved(TorrentImpl *const torrent, const QList<QUrl> &urlSeeds)
{
    for (const QUrl &urlSeed : urlSeeds)
        LogMsg(tr("Removed URL seed from torrent. Torrent: \"%1\". URL: \"%2\"").arg(torrent->name(), urlSeed.toString()));
}

void SessionImpl::handleTorrentMetadataReceived(TorrentImpl *const torrent)
{
    if (!torrentExportDirectory().isEmpty())
        exportTorrentFile(torrent, torrentExportDirectory());

    emit torrentMetadataReceived(torrent);
}

void SessionImpl::handleTorrentStopped(TorrentImpl *const torrent)
{
    torrent->resetTrackerEntryStatuses();

    const QList<TrackerEntryStatus> trackers = torrent->trackers();
    QHash<QString, TrackerEntryStatus> updatedTrackers;
    updatedTrackers.reserve(trackers.size());

    for (const TrackerEntryStatus &status : trackers)
        updatedTrackers.emplace(status.url, status);
    emit trackerEntryStatusesUpdated(torrent, updatedTrackers);

    LogMsg(tr("Torrent stopped. Torrent: \"%1\"").arg(torrent->name()));
    emit torrentStopped(torrent);
}

void SessionImpl::handleTorrentStarted(TorrentImpl *const torrent)
{
    LogMsg(tr("Torrent resumed. Torrent: \"%1\"").arg(torrent->name()));
    emit torrentStarted(torrent);
}

void SessionImpl::handleTorrentChecked(TorrentImpl *const torrent)
{
    emit torrentFinishedChecking(torrent);
}

void SessionImpl::handleTorrentFinished(TorrentImpl *const torrent)
{
    m_pendingFinishedTorrents.append(torrent);
}

void SessionImpl::handleTorrentResumeDataReady(TorrentImpl *const torrent, LoadTorrentParams data)
{
    m_resumeDataStorage->store(torrent->id(), std::move(data));
    const auto iter = m_changedTorrentIDs.constFind(torrent->id());
    if (iter != m_changedTorrentIDs.cend())
    {
        m_resumeDataStorage->remove(iter.value());
        m_changedTorrentIDs.erase(iter);
    }
}

void SessionImpl::handleTorrentInfoHashChanged(TorrentImpl *torrent, const InfoHash &prevInfoHash)
{
    Q_ASSERT(torrent->infoHash().isHybrid());

    m_hybridTorrentsByAltID.insert(TorrentID::fromSHA1Hash(torrent->infoHash().v1()), torrent);

    const auto prevID = TorrentID::fromInfoHash(prevInfoHash);
    const TorrentID currentID = torrent->id();
    if (currentID != prevID)
    {
        m_torrents[torrent->id()] = m_torrents.take(prevID);
        m_changedTorrentIDs[torrent->id()] = prevID;
    }
}

void SessionImpl::handleTorrentStorageMovingStateChanged(TorrentImpl *torrent)
{
    emit torrentsUpdated({torrent});
}

bool SessionImpl::addMoveTorrentStorageJob(TorrentImpl *torrent, const Path &newPath, const MoveStorageMode mode, const MoveStorageContext context)
{
    Q_ASSERT(torrent);

    const lt::torrent_handle torrentHandle = torrent->nativeHandle();
    const Path currentLocation = torrent->actualStorageLocation();
    const bool torrentHasActiveJob = !m_moveStorageQueue.isEmpty() && (m_moveStorageQueue.constFirst().torrentHandle == torrentHandle);

    if (m_moveStorageQueue.size() > 1)
    {
        const auto iter = std::ranges::find_if(std::views::drop(asConst(m_moveStorageQueue), 1)
                , [&torrentHandle](const MoveStorageJob &job)
        {
            return job.torrentHandle == torrentHandle;
        });

        if (iter != m_moveStorageQueue.cend())
        {
            // remove existing inactive job
            torrent->handleMoveStorageJobFinished(currentLocation, iter->context, torrentHasActiveJob);
            LogMsg(tr("Torrent move canceled. Torrent: \"%1\". Source: \"%2\". Destination: \"%3\"").arg(torrent->name(), currentLocation.toString(), iter->path.toString()));
            m_moveStorageQueue.erase(iter);
        }
    }

    if (torrentHasActiveJob)
    {
        // if there is active job for this torrent prevent creating meaningless
        // job that will move torrent to the same location as current one
        if (m_moveStorageQueue.constFirst().path == newPath)
        {
            LogMsg(tr("Failed to enqueue torrent move. Torrent: \"%1\". Source: \"%2\". Destination: \"%3\". Reason: torrent is currently moving to the destination")
                   .arg(torrent->name(), currentLocation.toString(), newPath.toString()));
            return false;
        }
    }
    else
    {
        if (currentLocation == newPath)
        {
            LogMsg(tr("Failed to enqueue torrent move. Torrent: \"%1\". Source: \"%2\" Destination: \"%3\". Reason: both paths point to the same location")
                   .arg(torrent->name(), currentLocation.toString(), newPath.toString()));
            return false;
        }
    }

    const MoveStorageJob moveStorageJob {torrentHandle, newPath, mode, context};
    m_moveStorageQueue << moveStorageJob;
    LogMsg(tr("Enqueued torrent move. Torrent: \"%1\". Source: \"%2\". Destination: \"%3\"").arg(torrent->name(), currentLocation.toString(), newPath.toString()));

    if (m_moveStorageQueue.size() == 1)
        moveTorrentStorage(moveStorageJob);

    return true;
}

lt::torrent_handle SessionImpl::reloadTorrent(const lt::torrent_handle &currentHandle, lt::add_torrent_params params)
{
    m_nativeSession->remove_torrent(currentHandle, lt::session::delete_partfile);

    auto *const extensionData = new ExtensionData;
    params.userdata = LTClientData(extensionData);
#ifndef QBT_USES_LIBTORRENT2
    params.storage = customStorageConstructor;
#endif

    // libtorrent will post an add_torrent_alert anyway, so we have to add an empty handler to ignore it.
    m_addTorrentAlertHandlers.emplaceBack();
    return m_nativeSession->add_torrent(std::move(params));
}

void SessionImpl::moveTorrentStorage(const MoveStorageJob &job) const
{
    const TorrentImpl *torrent = getTorrent(job.torrentHandle);
    const QString torrentName = (torrent ? torrent->name() : getInfoHash(job.torrentHandle).toTorrentID().toString());
    LogMsg(tr("Start moving torrent. Torrent: \"%1\". Destination: \"%2\"").arg(torrentName, job.path.toString()));

    job.torrentHandle.move_storage(job.path.toString().toStdString(), toNative(job.mode));
}

void SessionImpl::handleMoveTorrentStorageJobFinished(const Path &newPath)
{
    const MoveStorageJob finishedJob = m_moveStorageQueue.takeFirst();
    if (!m_moveStorageQueue.isEmpty())
        moveTorrentStorage(m_moveStorageQueue.constFirst());

    const auto iter = std::ranges::find_if(asConst(m_moveStorageQueue)
            , [&finishedJob](const MoveStorageJob &job)
    {
        return job.torrentHandle == finishedJob.torrentHandle;
    });

    const bool torrentHasOutstandingJob = (iter != m_moveStorageQueue.cend());

    TorrentImpl *torrent = getTorrent(finishedJob.torrentHandle);
    if (torrent)
    {
        torrent->handleMoveStorageJobFinished(newPath, finishedJob.context, torrentHasOutstandingJob);
    }
    else if (!torrentHasOutstandingJob)
    {
        // Last job is completed for torrent that being removing, so actually remove it
        const lt::torrent_handle nativeHandle = finishedJob.torrentHandle;
        const TorrentID torrentID = getInfoHash(nativeHandle).toTorrentID();
        const RemovingTorrentData &removingTorrentData = m_removingTorrents[torrentID];
        if (removingTorrentData.removeOption == TorrentRemoveOption::KeepContent)
            m_nativeSession->remove_torrent(nativeHandle, lt::session::delete_partfile);
    }
}

void SessionImpl::processPendingFinishedTorrents()
{
    if (m_pendingFinishedTorrents.isEmpty())
        return;

    for (TorrentImpl *torrent : asConst(m_pendingFinishedTorrents))
    {
        LogMsg(tr("Torrent download finished. Torrent: \"%1\"").arg(torrent->name()));
        emit torrentFinished(torrent);

        if (const Path exportPath = finishedTorrentExportDirectory(); !exportPath.isEmpty())
            exportTorrentFile(torrent, exportPath);

        processTorrentShareLimits(torrent);
    }

    m_pendingFinishedTorrents.clear();

    const bool hasUnfinishedTorrents = std::ranges::any_of(asConst(m_torrents), [](const TorrentImpl *torrent)
    {
        return !(torrent->isFinished() || torrent->isStopped() || torrent->isErrored());
    });
    if (!hasUnfinishedTorrents)
        emit allTorrentsFinished();
}

void SessionImpl::storeCategories() const
{
    QJsonObject jsonObj;
    for (auto it = m_categories.cbegin(); it != m_categories.cend(); ++it)
    {
        const QString &categoryName = it.key();
        const CategoryOptions &categoryOptions = it.value();
        jsonObj[categoryName] = categoryOptions.toJSON();
    }

    const Path path = specialFolderLocation(SpecialFolder::Config) / CATEGORIES_FILE_NAME;
    const QByteArray data = QJsonDocument(jsonObj).toJson();
    const nonstd::expected<void, QString> result = Utils::IO::saveToFile(path, data);
    if (!result)
    {
        LogMsg(tr("Failed to save Categories configuration. File: \"%1\". Error: \"%2\"")
               .arg(path.toString(), result.error()), Log::WARNING);
    }
}

void SessionImpl::upgradeCategories()
{
    const auto legacyCategories = SettingValue<QVariantMap>(u"BitTorrent/Session/Categories"_s).get();
    for (auto it = legacyCategories.cbegin(); it != legacyCategories.cend(); ++it)
    {
        const QString &categoryName = it.key();
        CategoryOptions categoryOptions;
        categoryOptions.savePath = Path(it.value().toString());
        m_categories[categoryName] = categoryOptions;
    }

    storeCategories();
}

void SessionImpl::loadCategories()
{
    m_categories.clear();

    const Path path = specialFolderLocation(SpecialFolder::Config) / CATEGORIES_FILE_NAME;
    if (!path.exists())
    {
        // TODO: Remove the following upgrade code in v4.5
        // == BEGIN UPGRADE CODE ==
        upgradeCategories();
        m_needUpgradeDownloadPath = true;
        // == END UPGRADE CODE ==

//        return;
    }

    const int fileMaxSize = 1024 * 1024;
    const auto readResult = Utils::IO::readFile(path, fileMaxSize);
    if (!readResult)
    {
        LogMsg(tr("Failed to load Categories. %1").arg(readResult.error().message), Log::WARNING);
        return;
    }

    QJsonParseError jsonError;
    const QJsonDocument jsonDoc = QJsonDocument::fromJson(readResult.value(), &jsonError);
    if (jsonError.error != QJsonParseError::NoError)
    {
        LogMsg(tr("Failed to parse Categories configuration. File: \"%1\". Error: \"%2\"")
               .arg(path.toString(), jsonError.errorString()), Log::WARNING);
        return;
    }

    if (!jsonDoc.isObject())
    {
        LogMsg(tr("Failed to load Categories configuration. File: \"%1\". Error: \"Invalid data format\"")
               .arg(path.toString()), Log::WARNING);
        return;
    }

    const QJsonObject jsonObj = jsonDoc.object();
    for (auto it = jsonObj.constBegin(); it != jsonObj.constEnd(); ++it)
    {
        const QString &categoryName = it.key();
        const auto categoryOptions = CategoryOptions::fromJSON(it.value().toObject());
        m_categories[categoryName] = categoryOptions;
    }

    m_categories = expandCategories(m_categories);
}

bool SessionImpl::hasPerTorrentRatioLimit() const
{
    return std::ranges::any_of(asConst(m_torrents), [](const TorrentImpl *torrent)
    {
        return (torrent->ratioLimit() >= 0);
    });
}

bool SessionImpl::hasPerTorrentSeedingTimeLimit() const
{
    return std::ranges::any_of(asConst(m_torrents), [](const TorrentImpl *torrent)
    {
        return (torrent->seedingTimeLimit() >= 0);
    });
}

bool SessionImpl::hasPerTorrentInactiveSeedingTimeLimit() const
{
    return std::ranges::any_of(asConst(m_torrents), [](const TorrentImpl *torrent)
    {
       return (torrent->inactiveSeedingTimeLimit() >= 0);
    });
}

void SessionImpl::configureDeferred()
{
    if (m_deferredConfigureScheduled)
        return;

    m_deferredConfigureScheduled = true;
    QMetaObject::invokeMethod(this, qOverload<>(&SessionImpl::configure), Qt::QueuedConnection);
}

// Enable IP Filtering
// this method creates ban list from scratch combining user ban list and 3rd party ban list file
void SessionImpl::enableIPFilter()
{
    qDebug("Enabling IPFilter");
    // 1. Parse the IP filter
    // 2. In the slot add the manually banned IPs to the provided lt::ip_filter
    // 3. Set the ip_filter in one go so there isn't a time window where there isn't an ip_filter
    //    set between clearing the old one and setting the new one.
    if (!m_filterParser)
    {
        m_filterParser = new FilterParserThread(this);
        connect(m_filterParser.data(), &FilterParserThread::IPFilterParsed, this, &SessionImpl::handleIPFilterParsed);
        connect(m_filterParser.data(), &FilterParserThread::IPFilterError, this, &SessionImpl::handleIPFilterError);
    }
    m_filterParser->processFilterFile(IPFilterFile());
}

// Disable IP Filtering
void SessionImpl::disableIPFilter()
{
    qDebug("Disabling IPFilter");
    if (m_filterParser)
    {
        disconnect(m_filterParser.data(), nullptr, this, nullptr);
        delete m_filterParser;
    }

    // Add the banned IPs after the IPFilter disabling
    // which creates an empty filter and overrides all previously
    // applied bans.
    lt::ip_filter filter;
    processBannedIPs(filter);
    m_nativeSession->set_ip_filter(filter);
}

const SessionStatus &SessionImpl::status() const
{
    return m_status;
}

const CacheStatus &SessionImpl::cacheStatus() const
{
    return m_cacheStatus;
}

void SessionImpl::enqueueRefresh()
{
    Q_ASSERT(!m_refreshEnqueued);

    QTimer::singleShot(refreshInterval(), Qt::CoarseTimer, this, [this]
    {
        m_nativeSession->post_torrent_updates();
        m_nativeSession->post_session_stats();

        if (m_torrentsQueueChanged)
        {
            m_torrentsQueueChanged = false;
            m_needSaveTorrentsQueue = true;
        }
    });

    m_refreshEnqueued = true;
}

void SessionImpl::handleIPFilterParsed(const int ruleCount)
{
    if (m_filterParser)
    {
        lt::ip_filter filter = m_filterParser->IPfilter();
        processBannedIPs(filter);
        m_nativeSession->set_ip_filter(filter);
    }
    LogMsg(tr("Successfully parsed the IP filter file. Number of rules applied: %1").arg(ruleCount));
    emit IPFilterParsed(false, ruleCount);
}

void SessionImpl::handleIPFilterError()
{
    lt::ip_filter filter;
    processBannedIPs(filter);
    m_nativeSession->set_ip_filter(filter);

    LogMsg(tr("Failed to parse the IP filter file"), Log::WARNING);
    emit IPFilterParsed(true, 0);
}

void SessionImpl::fetchPendingAlerts(const lt::time_duration time)
{
    if (time > lt::time_duration::zero())
        m_nativeSession->wait_for_alert(time);

    m_alerts.clear();
    m_nativeSession->pop_alerts(&m_alerts);
}

void SessionImpl::endAlertSequence(const int alertType, const qsizetype alertCount)
{
    if (alertType == lt::add_torrent_alert::alert_type)
    {
        emit addTorrentAlertsReceived(alertCount);

        if (!m_loadedTorrents.isEmpty())
        {
            if (isRestored())
                m_torrentsQueueChanged = true;

            emit torrentsLoaded(m_loadedTorrents);
            m_loadedTorrents.clear();
        }
    }
}

TorrentContentLayout SessionImpl::torrentContentLayout() const
{
    return m_torrentContentLayout;
}

void SessionImpl::setTorrentContentLayout(const TorrentContentLayout value)
{
    m_torrentContentLayout = value;
}

// Read alerts sent by libtorrent session
void SessionImpl::readAlerts()
{
    fetchPendingAlerts();

    Q_ASSERT(m_loadedTorrents.isEmpty());

    if (!isRestored())
        m_loadedTorrents.reserve(MAX_PROCESSING_RESUMEDATA_COUNT);

    int previousAlertType = -1;
    qsizetype alertSequenceSize = 0;
    for (lt::alert *a : m_alerts)
    {
        const int alertType = a->type();
        if ((alertType != previousAlertType) && (previousAlertType != -1))
        {
            endAlertSequence(previousAlertType, alertSequenceSize);
            alertSequenceSize = 0;
        }

        handleAlert(a);
        ++alertSequenceSize;
        previousAlertType = alertType;
    }
    endAlertSequence(previousAlertType, alertSequenceSize);

    // Some torrents may become "finished" after different alerts handling.
    processPendingFinishedTorrents();
}

void SessionImpl::handleAddTorrentAlert(const lt::add_torrent_alert *alert)
{
    Q_ASSERT(!m_addTorrentAlertHandlers.isEmpty());
    if (m_addTorrentAlertHandlers.isEmpty()) [[unlikely]]
        return;

    if (const AddTorrentAlertHandler handleAlert = m_addTorrentAlertHandlers.takeFirst())
        handleAlert(alert);
}

void SessionImpl::handleAlert(lt::alert *alert)
{
    try
    {
        switch (alert->type())
        {
#ifdef QBT_USES_LIBTORRENT2
        case lt::file_prio_alert::alert_type:
            handleFilePrioAlert(static_cast<const lt::file_prio_alert *>(alert));
            break;
#endif
        case lt::file_renamed_alert::alert_type:
            handleFileRenamedAlert(static_cast<const lt::file_renamed_alert *>(alert));
            break;
        case lt::file_rename_failed_alert::alert_type:
            handleFileRenameFailedAlert(static_cast<const lt::file_rename_failed_alert *>(alert));
            break;
        case lt::file_completed_alert::alert_type:
            handleFileCompletedAlert(static_cast<const lt::file_completed_alert *>(alert));
            break;
        case lt::file_error_alert::alert_type:
            handleFileErrorAlert(static_cast<const lt::file_error_alert *>(alert));
            break;
        case lt::torrent_finished_alert::alert_type:
            handleTorrentFinishedAlert(static_cast<const lt::torrent_finished_alert *>(alert));
            break;
        case lt::save_resume_data_alert::alert_type:
            handleSaveResumeDataAlert(static_cast<lt::save_resume_data_alert *>(alert));
            break;
        case lt::save_resume_data_failed_alert::alert_type:
            handleSaveResumeDataFailedAlert(static_cast<const lt::save_resume_data_failed_alert *>(alert));
            break;
        case lt::metadata_received_alert::alert_type:
            handleMetadataReceivedAlert(static_cast<const lt::metadata_received_alert *>(alert));
            break;
        case lt::fastresume_rejected_alert::alert_type:
            handleFastResumeRejectedAlert(static_cast<const lt::fastresume_rejected_alert *>(alert));
            break;
        case lt::torrent_checked_alert::alert_type:
            handleTorrentCheckedAlert(static_cast<const lt::torrent_checked_alert *>(alert));
            break;
        case lt::performance_alert::alert_type:
            handlePerformanceAlert(static_cast<const lt::performance_alert *>(alert));
            break;
        case lt::state_update_alert::alert_type:
            handleStateUpdateAlert(static_cast<const lt::state_update_alert *>(alert));
            break;
        case lt::session_error_alert::alert_type:
            handleSessionErrorAlert(static_cast<const lt::session_error_alert *>(alert));
            break;
        case lt::session_stats_alert::alert_type:
            handleSessionStatsAlert(static_cast<const lt::session_stats_alert *>(alert));
            break;
        case lt::tracker_announce_alert::alert_type:
        case lt::tracker_error_alert::alert_type:
        case lt::tracker_reply_alert::alert_type:
        case lt::tracker_warning_alert::alert_type:
            handleTrackerAlert(static_cast<const lt::tracker_alert *>(alert));
            break;
        case lt::add_torrent_alert::alert_type:
            handleAddTorrentAlert(static_cast<const lt::add_torrent_alert *>(alert));
            break;
        case lt::torrent_removed_alert::alert_type:
            handleTorrentRemovedAlert(static_cast<const lt::torrent_removed_alert *>(alert));
            break;
        case lt::torrent_deleted_alert::alert_type:
            handleTorrentDeletedAlert(static_cast<const lt::torrent_deleted_alert *>(alert));
            break;
        case lt::torrent_delete_failed_alert::alert_type:
            handleTorrentDeleteFailedAlert(static_cast<const lt::torrent_delete_failed_alert *>(alert));
            break;
        case lt::torrent_need_cert_alert::alert_type:
            handleTorrentNeedCertAlert(static_cast<const lt::torrent_need_cert_alert *>(alert));
            break;
        case lt::portmap_error_alert::alert_type:
            handlePortmapWarningAlert(static_cast<const lt::portmap_error_alert *>(alert));
            break;
        case lt::portmap_alert::alert_type:
            handlePortmapAlert(static_cast<const lt::portmap_alert *>(alert));
            break;
        case lt::peer_blocked_alert::alert_type:
            handlePeerBlockedAlert(static_cast<const lt::peer_blocked_alert *>(alert));
            break;
        case lt::peer_ban_alert::alert_type:
            handlePeerBanAlert(static_cast<const lt::peer_ban_alert *>(alert));
            break;
        case lt::url_seed_alert::alert_type:
            handleUrlSeedAlert(static_cast<const lt::url_seed_alert *>(alert));
            break;
        case lt::listen_succeeded_alert::alert_type:
            handleListenSucceededAlert(static_cast<const lt::listen_succeeded_alert *>(alert));
            break;
        case lt::listen_failed_alert::alert_type:
            handleListenFailedAlert(static_cast<const lt::listen_failed_alert *>(alert));
            break;
        case lt::external_ip_alert::alert_type:
            handleExternalIPAlert(static_cast<const lt::external_ip_alert *>(alert));
            break;
        case lt::alerts_dropped_alert::alert_type:
            handleAlertsDroppedAlert(static_cast<const lt::alerts_dropped_alert *>(alert));
            break;
        case lt::storage_moved_alert::alert_type:
            handleStorageMovedAlert(static_cast<const lt::storage_moved_alert *>(alert));
            break;
        case lt::storage_moved_failed_alert::alert_type:
            handleStorageMovedFailedAlert(static_cast<const lt::storage_moved_failed_alert *>(alert));
            break;
        case lt::socks5_alert::alert_type:
            handleSocks5Alert(static_cast<const lt::socks5_alert *>(alert));
            break;
        case lt::i2p_alert::alert_type:
            handleI2PAlert(static_cast<const lt::i2p_alert *>(alert));
            break;
#ifdef QBT_USES_LIBTORRENT2
        case lt::torrent_conflict_alert::alert_type:
            handleTorrentConflictAlert(static_cast<const lt::torrent_conflict_alert *>(alert));
            break;
#endif
        }
    }
    catch (const std::exception &exc)
    {
        qWarning() << "Caught exception in " << Q_FUNC_INFO << ": " << QString::fromStdString(exc.what());
    }
}

TorrentImpl *SessionImpl::createTorrent(const lt::torrent_handle &nativeHandle, LoadTorrentParams params)
{
    auto *const torrent = new TorrentImpl(this, nativeHandle, std::move(params));
    m_torrents.insert(torrent->id(), torrent);
    if (const InfoHash infoHash = torrent->infoHash(); infoHash.isHybrid())
        m_hybridTorrentsByAltID.insert(TorrentID::fromSHA1Hash(infoHash.v1()), torrent);

    if (((torrent->ratioLimit() >= 0) || (torrent->seedingTimeLimit() >= 0))
        && !m_seedingLimitTimer->isActive())
    {
        m_seedingLimitTimer->start();
    }

    // Torrent could have error just after adding to libtorrent
    if (torrent->hasError())
        LogMsg(tr("Torrent errored. Torrent: \"%1\". Error: \"%2\"").arg(torrent->name(), torrent->error()), Log::WARNING);

    return torrent;
}

TorrentImpl *SessionImpl::getTorrent(const lt::torrent_handle &nativeHandle) const
{
    return m_torrents.value(getInfoHash(nativeHandle).toTorrentID());
}

QList<TorrentImpl *> SessionImpl::getQueuedTorrentsByID(const QList<TorrentID> &torrentIDs) const
{
    QList<TorrentImpl *> queuedTorrents;
    queuedTorrents.reserve(torrentIDs.size());
    for (const TorrentID &torrentID : torrentIDs)
    {
        TorrentImpl *torrent = m_torrents.value(torrentID);
        if (torrent && (torrent->queuePosition() >= 0))
            queuedTorrents.push_back(torrent);
    }
    std::ranges::sort(queuedTorrents, std::less(), &TorrentImpl::queuePosition);
    return queuedTorrents;
}

void SessionImpl::handleTorrentRemovedAlert(const lt::torrent_removed_alert */*alert*/)
{
    // We cannot consider `torrent_removed_alert` as a starting point for removing content,
    // because it has an inconsistent posting time between different versions of libtorrent,
    // so files may still be in use in some cases.
}

void SessionImpl::handleTorrentDeletedAlert(const lt::torrent_deleted_alert *alert)
{
    handleRemovedTorrent(getInfoHash(*alert).toTorrentID());
}

void SessionImpl::handleTorrentDeleteFailedAlert(const lt::torrent_delete_failed_alert *alert)
{
    const TorrentID torrentID = getInfoHash(*alert).toTorrentID();
    const auto errorMessage = alert->error ? Utils::String::fromLocal8Bit(alert->error.message()) : QString();
    handleRemovedTorrent(torrentID, errorMessage);
}

void SessionImpl::handleTorrentNeedCertAlert(const lt::torrent_need_cert_alert *alert)
{
    const TorrentID torrentID = getInfoHash(alert->handle).toTorrentID();
    TorrentImpl *const torrent = m_torrents.value(torrentID);
    if (!torrent) [[unlikely]]
        return;

    if (!torrent->applySSLParameters())
    {
        LogMsg(tr("Torrent is missing SSL parameters. Torrent: \"%1\". Message: \"%2\"").arg(torrent->name(), QString::fromStdString(alert->message()))
            , Log::WARNING);
    }
}

void SessionImpl::handleMetadataReceivedAlert(const lt::metadata_received_alert *alert)
{
    TorrentImpl *torrent = getTorrent(alert->handle);
#ifdef QBT_USES_LIBTORRENT2
    if (!torrent && (alert->type() == lt::metadata_received_alert::alert_type))
    {
        const InfoHash infoHash = getInfoHash(alert->handle);
        if (infoHash.isHybrid())
            torrent = m_torrents.value(TorrentID::fromSHA1Hash(infoHash.v1()));
    }
#endif

    if (torrent)
        return torrent->handleMetadataReceived();

    const InfoHash infoHash = getInfoHash(alert->handle);
    const TorrentID torrentID = infoHash.toTorrentID();

    bool found = false;
    if (const auto iter = m_downloadedMetadata.constFind(torrentID); iter != m_downloadedMetadata.cend())
    {
        found = true;
        m_downloadedMetadata.erase(iter);
    }
#ifdef QBT_USES_LIBTORRENT2
    if (infoHash.isHybrid())
    {
        const auto altID = TorrentID::fromSHA1Hash(infoHash.v1());
        if (const auto iter = m_downloadedMetadata.constFind(altID); iter != m_downloadedMetadata.cend())
        {
            found = true;
            m_downloadedMetadata.erase(iter);
        }
    }
#endif
    if (found)
    {
        const TorrentInfo metadata {*alert->handle.torrent_file()};
        m_nativeSession->remove_torrent(alert->handle, lt::session::delete_files);

        emit metadataDownloaded(metadata);
    }
}

void SessionImpl::handleFileErrorAlert(const lt::file_error_alert *alert)
{
    TorrentImpl *const torrent = getTorrent(alert->handle);
    if (!torrent) [[unlikely]]
        return;

    torrent->handleFileError({.error = alert->error, .operation = alert->op});

    const TorrentID id = torrent->id();
    if (!m_recentErroredTorrents.contains(id))
    {
        m_recentErroredTorrents.insert(id);

        const QString msg = QString::fromStdString(alert->message());
        LogMsg(tr("File error alert. Torrent: \"%1\". File: \"%2\". Reason: \"%3\"")
                .arg(torrent->name(), QString::fromUtf8(alert->filename()), msg)
            , Log::WARNING);
        emit fullDiskError(torrent, msg);
    }

    m_recentErroredTorrentsTimer->start();
}

void SessionImpl::handlePortmapWarningAlert(const lt::portmap_error_alert *alert)
{
    LogMsg(tr("UPnP/NAT-PMP port mapping failed. Message: \"%1\"").arg(QString::fromStdString(alert->message())), Log::WARNING);
}

void SessionImpl::handlePortmapAlert(const lt::portmap_alert *alert)
{
    qDebug("UPnP Success, msg: %s", alert->message().c_str());
    LogMsg(tr("UPnP/NAT-PMP port mapping succeeded. Message: \"%1\"").arg(QString::fromStdString(alert->message())), Log::INFO);
}

void SessionImpl::handlePeerBlockedAlert(const lt::peer_blocked_alert *alert)
{
    QString reason;
    switch (alert->reason)
    {
    case lt::peer_blocked_alert::ip_filter:
        reason = tr("IP filter", "this peer was blocked. Reason: IP filter.");
        break;
    case lt::peer_blocked_alert::port_filter:
        reason = tr("filtered port (%1)", "this peer was blocked. Reason: filtered port (8899).").arg(QString::number(alert->endpoint.port()));
        break;
    case lt::peer_blocked_alert::i2p_mixed:
        reason = tr("%1 mixed mode restrictions", "this peer was blocked. Reason: I2P mixed mode restrictions.").arg(u"I2P"_s); // don't translate I2P
        break;
    case lt::peer_blocked_alert::privileged_ports:
        reason = tr("privileged port (%1)", "this peer was blocked. Reason: privileged port (80).").arg(QString::number(alert->endpoint.port()));
        break;
    case lt::peer_blocked_alert::utp_disabled:
        reason = tr("%1 is disabled", "this peer was blocked. Reason: uTP is disabled.").arg(C_UTP); // don't translate μTP
        break;
    case lt::peer_blocked_alert::tcp_disabled:
        reason = tr("%1 is disabled", "this peer was blocked. Reason: TCP is disabled.").arg(u"TCP"_s); // don't translate TCP
        break;
    }

    const QString ip {toString(alert->endpoint.address())};
    if (!ip.isEmpty())
        Logger::instance()->addPeer(ip, true, reason);
}

void SessionImpl::handlePeerBanAlert(const lt::peer_ban_alert *alert)
{
    const QString ip {toString(alert->endpoint.address())};
    if (!ip.isEmpty())
        Logger::instance()->addPeer(ip, false);
}

void SessionImpl::handleUrlSeedAlert(const lt::url_seed_alert *alert)
{
    const TorrentImpl *torrent = getTorrent(alert->handle);
    if (!torrent)
        return;

    if (alert->error)
    {
        LogMsg(tr("URL seed connection failed. Torrent: \"%1\". URL: \"%2\". Error: \"%3\"")
            .arg(torrent->name(), QString::fromUtf8(alert->server_url()), QString::fromStdString(alert->message()))
            , Log::WARNING);
    }
    else
    {
        LogMsg(tr("Received error message from URL seed. Torrent: \"%1\". URL: \"%2\". Message: \"%3\"")
            .arg(torrent->name(), QString::fromUtf8(alert->server_url()), QString::fromUtf8(alert->error_message()))
            , Log::WARNING);
    }
}

void SessionImpl::handleListenSucceededAlert(const lt::listen_succeeded_alert *alert)
{
    const QString proto {toString(alert->socket_type)};
    LogMsg(tr("Successfully listening on IP. IP: \"%1\". Port: \"%2/%3\"")
            .arg(toString(alert->address), proto, QString::number(alert->port)), Log::INFO);
}

void SessionImpl::handleListenFailedAlert(const lt::listen_failed_alert *alert)
{
    const QString proto {toString(alert->socket_type)};
    LogMsg(tr("Failed to listen on IP. IP: \"%1\". Port: \"%2/%3\". Reason: \"%4\"")
        .arg(toString(alert->address), proto, QString::number(alert->port)
            , Utils::String::fromLocal8Bit(alert->error.message())), Log::CRITICAL);
}

void SessionImpl::handleExternalIPAlert(const lt::external_ip_alert *alert)
{
    const QString externalIP {toString(alert->external_address)};
    LogMsg(tr("Detected external IP. IP: \"%1\"")
        .arg(externalIP), Log::INFO);

    const bool isIPv6 = alert->external_address.is_v6();
    const bool isIPv4 = alert->external_address.is_v4();
    if (isIPv6 && (externalIP != m_lastExternalIPv6Address))
    {
        if (isReannounceWhenAddressChangedEnabled() && !m_lastExternalIPv6Address.isEmpty())
            reannounceToAllTrackers();
        m_lastExternalIPv6Address = externalIP;
    }
    else if (isIPv4 && (externalIP != m_lastExternalIPv4Address))
    {
        if (isReannounceWhenAddressChangedEnabled() && !m_lastExternalIPv4Address.isEmpty())
            reannounceToAllTrackers();
        m_lastExternalIPv4Address = externalIP;
    }
}

void SessionImpl::handleSessionErrorAlert(const lt::session_error_alert *alert) const
{
    LogMsg(tr("BitTorrent session encountered a serious error. Reason: \"%1\"")
        .arg(QString::fromStdString(alert->message())), Log::CRITICAL);
}

void SessionImpl::handleSessionStatsAlert(const lt::session_stats_alert *alert)
{
    if (m_refreshEnqueued)
        m_refreshEnqueued = false;
    else
        enqueueRefresh();

    const int64_t interval = lt::total_microseconds(alert->timestamp() - m_statsLastTimestamp);
    if (interval <= 0)
        return;

    m_statsLastTimestamp = alert->timestamp();

    const auto stats = alert->counters();

    m_status.hasIncomingConnections = static_cast<bool>(stats[m_metricIndices.net.hasIncomingConnections]);

    const int64_t ipOverheadDownload = stats[m_metricIndices.net.recvIPOverheadBytes];
    const int64_t ipOverheadUpload = stats[m_metricIndices.net.sentIPOverheadBytes];
    const int64_t totalDownload = stats[m_metricIndices.net.recvBytes] + ipOverheadDownload;
    const int64_t totalUpload = stats[m_metricIndices.net.sentBytes] + ipOverheadUpload;
    const int64_t totalPayloadDownload = stats[m_metricIndices.net.recvPayloadBytes];
    const int64_t totalPayloadUpload = stats[m_metricIndices.net.sentPayloadBytes];
    const int64_t trackerDownload = stats[m_metricIndices.net.recvTrackerBytes];
    const int64_t trackerUpload = stats[m_metricIndices.net.sentTrackerBytes];
    const int64_t dhtDownload = stats[m_metricIndices.dht.dhtBytesIn];
    const int64_t dhtUpload = stats[m_metricIndices.dht.dhtBytesOut];

    const auto calcRate = [interval](const qint64 previous, const qint64 current) -> qint64
    {
        Q_ASSERT(current >= previous);
        Q_ASSERT(interval >= 0);
        return (((current - previous) * lt::microseconds(1s).count()) / interval);
    };

    m_status.payloadDownloadRate = calcRate(m_status.totalPayloadDownload, totalPayloadDownload);
    m_status.payloadUploadRate = calcRate(m_status.totalPayloadUpload, totalPayloadUpload);
    m_status.downloadRate = calcRate(m_status.totalDownload, totalDownload);
    m_status.uploadRate = calcRate(m_status.totalUpload, totalUpload);
    m_status.ipOverheadDownloadRate = calcRate(m_status.ipOverheadDownload, ipOverheadDownload);
    m_status.ipOverheadUploadRate = calcRate(m_status.ipOverheadUpload, ipOverheadUpload);
    m_status.dhtDownloadRate = calcRate(m_status.dhtDownload, dhtDownload);
    m_status.dhtUploadRate = calcRate(m_status.dhtUpload, dhtUpload);
    m_status.trackerDownloadRate = calcRate(m_status.trackerDownload, trackerDownload);
    m_status.trackerUploadRate = calcRate(m_status.trackerUpload, trackerUpload);

    m_status.totalPayloadDownload = totalPayloadDownload;
    m_status.totalPayloadUpload = totalPayloadUpload;
    m_status.ipOverheadDownload = ipOverheadDownload;
    m_status.ipOverheadUpload = ipOverheadUpload;
    m_status.trackerDownload = trackerDownload;
    m_status.trackerUpload = trackerUpload;
    m_status.dhtDownload = dhtDownload;
    m_status.dhtUpload = dhtUpload;
    m_status.totalWasted = stats[m_metricIndices.net.recvRedundantBytes]
            + stats[m_metricIndices.net.recvFailedBytes];
    m_status.dhtNodes = stats[m_metricIndices.dht.dhtNodes];
    m_status.diskReadQueue = stats[m_metricIndices.peer.numPeersUpDisk];
    m_status.diskWriteQueue = stats[m_metricIndices.peer.numPeersDownDisk];
    m_status.peersCount = stats[m_metricIndices.peer.numPeersConnected];

    if (totalDownload > m_status.totalDownload)
    {
        m_status.totalDownload = totalDownload;
        m_isStatisticsDirty = true;
    }

    if (totalUpload > m_status.totalUpload)
    {
        m_status.totalUpload = totalUpload;
        m_isStatisticsDirty = true;
    }

    m_status.allTimeDownload = m_previouslyDownloaded + m_status.totalDownload;
    m_status.allTimeUpload = m_previouslyUploaded + m_status.totalUpload;

    if (m_saveStatisticsInterval > 0)
    {
        const auto saveInterval = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::minutes(m_saveStatisticsInterval));
        if (m_statisticsLastUpdateTimer.hasExpired(saveInterval.count()))
        {
            saveStatistics();
        }
    }

    m_cacheStatus.totalUsedBuffers = stats[m_metricIndices.disk.diskBlocksInUse];
    m_cacheStatus.jobQueueLength = stats[m_metricIndices.disk.queuedDiskJobs];

#ifndef QBT_USES_LIBTORRENT2
    const int64_t numBlocksRead = stats[m_metricIndices.disk.numBlocksRead];
    const int64_t numBlocksCacheHits = stats[m_metricIndices.disk.numBlocksCacheHits];
    m_cacheStatus.readRatio = static_cast<qreal>(numBlocksCacheHits) / std::max<int64_t>((numBlocksCacheHits + numBlocksRead), 1);
#endif

    const int64_t totalJobs = stats[m_metricIndices.disk.writeJobs] + stats[m_metricIndices.disk.readJobs]
                  + stats[m_metricIndices.disk.hashJobs];
    m_cacheStatus.averageJobTime = (totalJobs > 0)
                                   ? (stats[m_metricIndices.disk.diskJobTime] / totalJobs) : 0;

    emit statsUpdated();
}

void SessionImpl::handleAlertsDroppedAlert(const lt::alerts_dropped_alert *alert) const
{
    LogMsg(tr("Error: Internal alert queue is full and alerts are dropped, you might see degraded performance. Dropped alert type: \"%1\". Message: \"%2\"")
        .arg(QString::fromStdString(alert->dropped_alerts.to_string()), QString::fromStdString(alert->message())), Log::CRITICAL);
}

void SessionImpl::handleStorageMovedAlert(const lt::storage_moved_alert *alert)
{
    Q_ASSERT(!m_moveStorageQueue.isEmpty());

    const MoveStorageJob &currentJob = m_moveStorageQueue.constFirst();
    Q_ASSERT(currentJob.torrentHandle == alert->handle);

    const Path newPath {QString::fromUtf8(alert->storage_path())};
    Q_ASSERT(newPath == currentJob.path);

    TorrentImpl *torrent = getTorrent(currentJob.torrentHandle);
    const QString torrentName = (torrent ? torrent->name() : getInfoHash(currentJob.torrentHandle).toTorrentID().toString());
    LogMsg(tr("Moved torrent successfully. Torrent: \"%1\". Destination: \"%2\"").arg(torrentName, newPath.toString()));

    handleMoveTorrentStorageJobFinished(newPath);
}

void SessionImpl::handleStorageMovedFailedAlert(const lt::storage_moved_failed_alert *alert)
{
    Q_ASSERT(!m_moveStorageQueue.isEmpty());

    const MoveStorageJob &currentJob = m_moveStorageQueue.constFirst();
    Q_ASSERT(currentJob.torrentHandle == alert->handle);

    TorrentImpl *torrent = getTorrent(currentJob.torrentHandle);
    const QString torrentName = (torrent ? torrent->name() : getInfoHash(currentJob.torrentHandle).toTorrentID().toString());
    const Path currentLocation = (torrent ? torrent->actualStorageLocation()
            : Path(alert->handle.status(lt::torrent_handle::query_save_path).save_path));
    const QString errorMessage = QString::fromStdString(alert->message());
    LogMsg(tr("Failed to move torrent. Torrent: \"%1\". Source: \"%2\". Destination: \"%3\". Reason: \"%4\"")
           .arg(torrentName, currentLocation.toString(), currentJob.path.toString(), errorMessage), Log::WARNING);

    handleMoveTorrentStorageJobFinished(currentLocation);
}

void SessionImpl::handleStateUpdateAlert(const lt::state_update_alert *alert)
{
    QList<Torrent *> updatedTorrents;
    updatedTorrents.reserve(static_cast<decltype(updatedTorrents)::size_type>(alert->status.size()));

    for (const lt::torrent_status &status : alert->status)
    {
        TorrentImpl *const torrent = getTorrent(status.handle);
        if (!torrent)
            continue;

        torrent->handleStateUpdate(status);
        updatedTorrents.push_back(torrent);
    }

    if (!updatedTorrents.isEmpty())
        emit torrentsUpdated(updatedTorrents);

    if (m_needSaveTorrentsQueue)
        saveTorrentsQueue();

    if (m_refreshEnqueued)
        m_refreshEnqueued = false;
    else
        enqueueRefresh();
}

void SessionImpl::handleSocks5Alert(const lt::socks5_alert *alert) const
{
    if (alert->error)
    {
        const auto addr = alert->ip.address();
        const QString endpoint = (addr.is_v6() ? u"[%1]:%2"_s : u"%1:%2"_s)
                .arg(QString::fromStdString(addr.to_string()), QString::number(alert->ip.port()));
        LogMsg(tr("SOCKS5 proxy error. Address: %1. Message: \"%2\".")
                .arg(endpoint, Utils::String::fromLocal8Bit(alert->error.message()))
                , Log::WARNING);
    }
}

void SessionImpl::handleI2PAlert(const lt::i2p_alert *alert) const
{
    if (alert->error)
    {
        LogMsg(tr("I2P error. Message: \"%1\".")
            .arg(QString::fromStdString(alert->message())), Log::WARNING);
    }
}

void SessionImpl::handleTrackerAlert(const lt::tracker_alert *alert)
{
    TorrentImpl *torrent = getTorrent(alert->handle);
    if (!torrent)
        return;

    [[maybe_unused]] const QMutexLocker updatedTrackerStatusesLocker {&m_updatedTrackerStatusesMutex};

    const auto prevSize = m_updatedTrackerStatuses.size();
    QMap<int, int> &updateInfo = m_updatedTrackerStatuses[torrent->nativeHandle()][std::string(alert->tracker_url())][alert->local_endpoint];
    if (prevSize < m_updatedTrackerStatuses.size())
        updateTrackerEntryStatuses(torrent->nativeHandle());

    if (alert->type() == lt::tracker_reply_alert::alert_type)
    {
        const int numPeers = static_cast<const lt::tracker_reply_alert *>(alert)->num_peers;
#ifdef QBT_USES_LIBTORRENT2
        const int protocolVersionNum = (static_cast<const lt::tracker_reply_alert *>(alert)->version == lt::protocol_version::V1) ? 1 : 2;
#else
        const int protocolVersionNum = 1;
#endif
        updateInfo.insert(protocolVersionNum, numPeers);
    }
}

#ifdef QBT_USES_LIBTORRENT2
void SessionImpl::handleTorrentConflictAlert(const lt::torrent_conflict_alert *alert)
{
    const InfoHash infoHash = getInfoHash(*alert->metadata);
    const auto torrentIDv1 = TorrentID::fromSHA1Hash(infoHash.v1());
    const auto torrentIDv2 = TorrentID::fromSHA256Hash(infoHash.v2());
    TorrentImpl *torrent1 = m_torrents.value(torrentIDv1);
    TorrentImpl *torrent2 = m_torrents.value(torrentIDv2);
    if (torrent2)
    {
        if (torrent1)
            removeTorrent(torrentIDv1);
        else
            cancelDownloadMetadata(torrentIDv1);

        invokeAsync([torrentHandle = torrent2->nativeHandle(), metadata = alert->metadata]
        {
            try
            {
                torrentHandle.set_metadata(metadata->info_section());
            }
            catch (const std::exception &) {}
        });
    }
    else if (torrent1)
    {
        if (!torrent2)
            cancelDownloadMetadata(torrentIDv2);

        invokeAsync([torrentHandle = torrent1->nativeHandle(), metadata = alert->metadata]
        {
            try
            {
                torrentHandle.set_metadata(metadata->info_section());
            }
            catch (const std::exception &) {}
        });
    }
    else
    {
        cancelDownloadMetadata(torrentIDv1);
        cancelDownloadMetadata(torrentIDv2);
    }

    if (!torrent1 || !torrent2)
        emit metadataDownloaded(TorrentInfo(*alert->metadata));
}

void SessionImpl::handleFilePrioAlert(const lt::file_prio_alert *alert)
{
    if (TorrentImpl *torrent = getTorrent(alert->handle)) [[likely]]
        torrent->deferredRequestResumeData();
}
#endif

void SessionImpl::handleTorrentCheckedAlert(const lt::torrent_checked_alert *alert)
{
    if (TorrentImpl *torrent = getTorrent(alert->handle)) [[likely]]
        torrent->handleTorrentChecked();
}

void SessionImpl::handleTorrentFinishedAlert([[maybe_unused]] const lt::torrent_finished_alert *alert)
{
    if (TorrentImpl *torrent = getTorrent(alert->handle)) [[likely]]
        torrent->handleTorrentFinished();
}

void SessionImpl::handleSaveResumeDataAlert(lt::save_resume_data_alert *alert)
{
    // The torrent can be deleted between the time the resume data was requested and
    // the time we received the appropriate alert. We have to decrease `m_numResumeData` anyway,
    // so we do this before checking for an existing torrent.
    --m_numResumeData;

    if (TorrentImpl *torrent = getTorrent(alert->handle)) [[likely]]
        torrent->handleSaveResumeData(std::move(alert->params));
}

void SessionImpl::handleSaveResumeDataFailedAlert(const lt::save_resume_data_failed_alert *alert)
{
    // The torrent can be deleted between the time the resume data was requested and
    // the time we received the appropriate alert. We have to decrease `m_numResumeData` anyway,
    // so we do this before checking for an existing torrent.
    --m_numResumeData;

    TorrentImpl *torrent = getTorrent(alert->handle);
    if (!torrent) [[unlikely]]
        return;

    if (alert->error != lt::errors::resume_data_not_modified)
    {
        LogMsg(tr("Generate resume data failed. Torrent: \"%1\". Reason: \"%2\"")
                .arg(torrent->name(), Utils::String::fromLocal8Bit(alert->error.message())), Log::CRITICAL);
    }
}

void SessionImpl::handleFastResumeRejectedAlert(const lt::fastresume_rejected_alert *alert)
{
    TorrentImpl *torrent = getTorrent(alert->handle);
    if (!torrent) [[unlikely]]
        return;

    torrent->handleFastResumeRejected();
    LogMsg(tr("Failed to restore torrent. Files were probably moved or storage isn't accessible. Torrent: \"%1\". Reason: \"%2\"")
            .arg(torrent->name(), QString::fromStdString(alert->message())), Log::WARNING);
}

void SessionImpl::handleFileRenamedAlert(const lt::file_renamed_alert *alert)
{
    TorrentImpl *torrent = getTorrent(alert->handle);
    if (!torrent) [[unlikely]]
        return;

    const Path newFilePath {QString::fromUtf8(alert->new_name())};
#ifdef QBT_USES_LIBTORRENT2
    const Path oldFilePath {QString::fromUtf8(alert->old_name())};
#else
    const Path oldFilePath;
#endif
    torrent->handleFileRenamed(alert->index, newFilePath, oldFilePath);
}

void SessionImpl::handleFileRenameFailedAlert(const lt::file_rename_failed_alert *alert)
{
    TorrentImpl *torrent = getTorrent(alert->handle);
    if (!torrent) [[unlikely]]
        return;

    torrent->handleFileRenameFailed(alert->index);

    LogMsg(tr("File rename failed. Torrent: \"%1\", file: \"%2\", reason: \"%3\"")
            .arg(torrent->name(), torrent->filePath(torrent->fileIndexFromNative(alert->index)).toString()
                    , Utils::String::fromLocal8Bit(alert->error.message())), Log::WARNING);
}

void SessionImpl::handleFileCompletedAlert(const lt::file_completed_alert *alert)
{
    if (TorrentImpl *torrent = getTorrent(alert->handle)) [[likely]]
        torrent->handleFileCompleted(alert->index);
}

void SessionImpl::handlePerformanceAlert(const lt::performance_alert *alert) const
{
    LogMsg((tr("Performance alert: %1. More info: %2").arg(QString::fromStdString(alert->message())
            , u"https://libtorrent.org/reference-Alerts.html#enum-performance-warning-t"_s)), Log::INFO);
}

void SessionImpl::saveStatistics() const
{
    if (!m_isStatisticsDirty)
        return;

    const QVariantHash stats {
        {u"AlltimeDL"_s, m_status.allTimeDownload},
        {u"AlltimeUL"_s, m_status.allTimeUpload}};
    std::unique_ptr<QSettings> settings = Profile::instance()->applicationSettings(u"qBittorrent-data"_s);
    settings->setValue(u"Stats/AllStats"_s, stats);

    m_statisticsLastUpdateTimer.start();
    m_isStatisticsDirty = false;
}

void SessionImpl::loadStatistics()
{
    const std::unique_ptr<QSettings> settings = Profile::instance()->applicationSettings(u"qBittorrent-data"_s);
    const QVariantHash value = settings->value(u"Stats/AllStats"_s).toHash();

    m_previouslyDownloaded = value[u"AlltimeDL"_s].toLongLong();
    m_previouslyUploaded = value[u"AlltimeUL"_s].toLongLong();
}

void SessionImpl::updateTrackerEntryStatuses(lt::torrent_handle torrentHandle)
{
    invokeAsync([this, torrentHandle = std::move(torrentHandle)]
    {
        try
        {
            std::vector<lt::announce_entry> nativeTrackers = torrentHandle.trackers();

            QMutexLocker updatedTrackerStatusesLocker {&m_updatedTrackerStatusesMutex};
            QHash<std::string, QHash<lt::tcp::endpoint, QMap<int, int>>> updatedTrackers = m_updatedTrackerStatuses.take(torrentHandle);
            updatedTrackerStatusesLocker.unlock();

            invoke([this, infoHash = torrentHandle.info_hash(), nativeTrackers = std::move(nativeTrackers), updatedTrackers = std::move(updatedTrackers)]
            {
                TorrentImpl *torrent = m_torrents.value(infoHash);
                if (!torrent || torrent->isStopped())
                    return;

                QHash<QString, TrackerEntryStatus> trackers;
                trackers.reserve(updatedTrackers.size());
                for (const lt::announce_entry &announceEntry : nativeTrackers)
                {
                    const auto updatedTrackersIter = updatedTrackers.find(announceEntry.url);
                    if (updatedTrackersIter == updatedTrackers.end())
                        continue;

                    const auto &updateInfo = updatedTrackersIter.value();
                    TrackerEntryStatus status = torrent->updateTrackerEntryStatus(announceEntry, updateInfo);
                    const QString url = status.url;
                    trackers.emplace(url, std::move(status));
                }

                emit trackerEntryStatusesUpdated(torrent, trackers);
            });
        }
        catch (const std::exception &)
        {
        }
    });
}

void SessionImpl::handleRemovedTorrent(const TorrentID &torrentID, const QString &partfileRemoveError)
{
    const auto removingTorrentDataIter = m_removingTorrents.constFind(torrentID);
    if (removingTorrentDataIter == m_removingTorrents.cend())
        return;

    if (!partfileRemoveError.isEmpty())
    {
        LogMsg(tr("Failed to remove partfile. Torrent: \"%1\". Reason: \"%2\".")
               .arg(removingTorrentDataIter->name, partfileRemoveError)
               , Log::WARNING);
    }

    if ((removingTorrentDataIter->removeOption == TorrentRemoveOption::RemoveContent)
            && !removingTorrentDataIter->contentStoragePath.isEmpty())
    {
        QMetaObject::invokeMethod(m_torrentContentRemover, [this, jobData = *removingTorrentDataIter]
        {
            m_torrentContentRemover->performJob(jobData.name, jobData.contentStoragePath
                    , jobData.fileNames, m_torrentContentRemoveOption);
        });
    }

    m_removingTorrents.erase(removingTorrentDataIter);
}

void SessionImpl::updateTrackersFromFile()
{
    const qint64 fileMaxSize = 1024 * 1024;
    const Path path = specialFolderLocation(SpecialFolder::Data) / Path(ADDITIONAL_TRACKERS_FROM_URL_FILE_NAME);

    const auto readResult = Utils::IO::readFile(path, fileMaxSize);
    if (!readResult)
    {
        if (readResult.error().status == Utils::IO::ReadError::NotExist)
        {
            return;
        }

        LogMsg(tr("Failed to load additional trackers from file. Reason: %1").arg(readResult.error().message), Log::WARNING);
        return;
    }

    setAdditionalTrackersFromURL(QString::fromUtf8(readResult.value()));
}
