/*
 * 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 "torrentimpl.h"

#include <algorithm>
#include <memory>

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

#include <libtorrent/address.hpp>
#include <libtorrent/session.hpp>
#include <libtorrent/storage_defs.hpp>
#include <libtorrent/time.hpp>
#include <libtorrent/write_resume_data.hpp>

#ifdef QBT_USES_LIBTORRENT2
#include <libtorrent/info_hash.hpp>
#endif

#include <QtSystemDetection>
#include <QByteArray>
#include <QCache>
#include <QDebug>
#include <QFuture>
#include <QPointer>
#include <QPromise>
#include <QSet>
#include <QStringList>
#include <QUrl>

#include "base/exceptions.h"
#include "base/global.h"
#include "base/logger.h"
#include "base/preferences.h"
#include "base/types.h"
#include "base/utils/fs.h"
#include "base/utils/io.h"
#include "base/utils/string.h"
#include "common.h"
#include "downloadpriority.h"
#include "extensiondata.h"
#include "filesearcher.h"
#include "loadtorrentparams.h"
#include "ltqbitarray.h"
#include "lttypecast.h"
#include "peeraddress.h"
#include "peerinfo.h"
#include "sessionimpl.h"
#include "trackerentry.h"

#if defined(Q_OS_MACOS) || defined(Q_OS_WIN)
#include "base/utils/os.h"
#endif // Q_OS_MACOS || Q_OS_WIN

#ifndef QBT_USES_LIBTORRENT2
#include "customstorage.h"
#endif

using namespace BitTorrent;

namespace
{
    lt::announce_entry makeNativeAnnounceEntry(const QString &url, const int tier)
    {
        lt::announce_entry entry {url.toStdString()};
        entry.tier = tier;
        return entry;
    }

    QString toString(const lt::tcp::endpoint &ltTCPEndpoint)
    {
        static QCache<lt::tcp::endpoint, QString> cache;

        if (const QString *endpointName = cache.object(ltTCPEndpoint))
            return *endpointName;

        const auto endpointName = Utils::String::fromLatin1((std::ostringstream() << ltTCPEndpoint).str());
        cache.insert(ltTCPEndpoint, new QString(endpointName));
        return endpointName;
    }

    void updateTrackerEntryStatus(TrackerEntryStatus &trackerEntryStatus, const lt::announce_entry &nativeEntry
            , const QSet<int> &btProtocols, const QHash<lt::tcp::endpoint, QMap<int, int>> &updateInfo)
    {
        Q_ASSERT(trackerEntryStatus.url == QString::fromStdString(nativeEntry.url));

        trackerEntryStatus.tier = nativeEntry.tier;

        const auto numEndpoints = static_cast<qsizetype>(nativeEntry.endpoints.size()) * btProtocols.size();

        int numUpdating = 0;
        int numWorking = 0;
        int numNotWorking = 0;
        int numTrackerError = 0;
        int numUnreachable = 0;

        for (const lt::announce_endpoint &ltAnnounceEndpoint : nativeEntry.endpoints)
        {
            const auto endpointName = toString(ltAnnounceEndpoint.local_endpoint);

            for (const auto protocolVersion : btProtocols)
            {
#ifdef QBT_USES_LIBTORRENT2
                Q_ASSERT((protocolVersion == 1) || (protocolVersion == 2));
                const auto ltProtocolVersion = (protocolVersion == 1) ? lt::protocol_version::V1 : lt::protocol_version::V2;
                const lt::announce_infohash &ltAnnounceInfo = ltAnnounceEndpoint.info_hashes[ltProtocolVersion];
#else
                Q_ASSERT(protocolVersion == 1);
                const lt::announce_endpoint &ltAnnounceInfo = ltAnnounceEndpoint;
#endif
                const QMap<int, int> &endpointUpdateInfo = updateInfo[ltAnnounceEndpoint.local_endpoint];
                TrackerEndpointStatus &trackerEndpointStatus = trackerEntryStatus.endpoints[std::make_pair(endpointName, protocolVersion)];

                trackerEndpointStatus.name = endpointName;
                trackerEndpointStatus.btVersion = protocolVersion;
                trackerEndpointStatus.numPeers = endpointUpdateInfo.value(protocolVersion, trackerEndpointStatus.numPeers);
                trackerEndpointStatus.numSeeds = ltAnnounceInfo.scrape_complete;
                trackerEndpointStatus.numLeeches = ltAnnounceInfo.scrape_incomplete;
                trackerEndpointStatus.numDownloaded = ltAnnounceInfo.scrape_downloaded;
                trackerEndpointStatus.nextAnnounceTime = ltAnnounceInfo.next_announce;
                trackerEndpointStatus.minAnnounceTime = ltAnnounceInfo.min_announce;

                if (ltAnnounceInfo.updating)
                {
                    trackerEndpointStatus.isUpdating = true;
                    ++numUpdating;
                }
                else
                {
                    trackerEndpointStatus.isUpdating = false;

                    if (ltAnnounceInfo.fails > 0)
                    {
                        if (ltAnnounceInfo.last_error == lt::errors::tracker_failure)
                        {
                            trackerEndpointStatus.state = TrackerEndpointState::TrackerError;
                            ++numTrackerError;
                        }
                        else if (ltAnnounceInfo.last_error == lt::errors::announce_skipped)
                        {
                            trackerEndpointStatus.state = TrackerEndpointState::Unreachable;
                            ++numUnreachable;
                        }
                        else
                        {
                            trackerEndpointStatus.state = TrackerEndpointState::NotWorking;
                            ++numNotWorking;
                        }
                    }
                    else if (nativeEntry.verified)
                    {
                        trackerEndpointStatus.state = TrackerEndpointState::Working;
                        ++numWorking;
                    }
                    else
                    {
                        trackerEndpointStatus.state = TrackerEndpointState::NotContacted;
                    }
                }

                if (!ltAnnounceInfo.message.empty())
                {
                    trackerEndpointStatus.message = QString::fromStdString(ltAnnounceInfo.message);
                }
                else if (ltAnnounceInfo.last_error)
                {
                    trackerEndpointStatus.message = QString::fromLocal8Bit(ltAnnounceInfo.last_error.message());
                }
                else
                {
                    trackerEndpointStatus.message.clear();
                }
            }
        }

        if (trackerEntryStatus.endpoints.size() > numEndpoints)
        {
            // remove outdated endpoints
            trackerEntryStatus.endpoints.removeIf([&nativeEntry](const QHash<std::pair<QString, int>, TrackerEndpointStatus>::iterator &iter)
            {
                return std::ranges::none_of(nativeEntry.endpoints
                        , [&endpointName = std::get<0>(iter.key())](const auto &existingEndpoint)
                {
                    return (endpointName == toString(existingEndpoint.local_endpoint));
                });
            });
        }

        if (numEndpoints > 0)
        {
            if (numUpdating > 0)
            {
                trackerEntryStatus.isUpdating = true;
            }
            else
            {
                trackerEntryStatus.isUpdating = false;

                if (numWorking > 0)
                {
                    trackerEntryStatus.state = TrackerEndpointState::Working;
                }
                else if (numTrackerError > 0)
                {
                    trackerEntryStatus.state = TrackerEndpointState::TrackerError;
                }
                else if (numUnreachable == numEndpoints)
                {
                    trackerEntryStatus.state = TrackerEndpointState::Unreachable;
                }
                else if ((numUnreachable + numNotWorking) == numEndpoints)
                {
                    trackerEntryStatus.state = TrackerEndpointState::NotWorking;
                }
            }
        }

        trackerEntryStatus.numPeers = -1;
        trackerEntryStatus.numSeeds = -1;
        trackerEntryStatus.numLeeches = -1;
        trackerEntryStatus.numDownloaded = -1;
        trackerEntryStatus.nextAnnounceTime = {};
        trackerEntryStatus.minAnnounceTime = {};
        trackerEntryStatus.message.clear();

        for (const TrackerEndpointStatus &endpointStatus : asConst(trackerEntryStatus.endpoints))
        {
            trackerEntryStatus.numPeers = std::max(trackerEntryStatus.numPeers, endpointStatus.numPeers);
            trackerEntryStatus.numSeeds = std::max(trackerEntryStatus.numSeeds, endpointStatus.numSeeds);
            trackerEntryStatus.numLeeches = std::max(trackerEntryStatus.numLeeches, endpointStatus.numLeeches);
            trackerEntryStatus.numDownloaded = std::max(trackerEntryStatus.numDownloaded, endpointStatus.numDownloaded);

            if (endpointStatus.state == trackerEntryStatus.state)
            {
                if ((trackerEntryStatus.nextAnnounceTime == AnnounceTimePoint()) || (trackerEntryStatus.nextAnnounceTime > endpointStatus.nextAnnounceTime))
                {
                    trackerEntryStatus.nextAnnounceTime = endpointStatus.nextAnnounceTime;
                    trackerEntryStatus.minAnnounceTime = endpointStatus.minAnnounceTime;
                    if ((endpointStatus.state != TrackerEndpointState::Working)
                            || !endpointStatus.message.isEmpty())
                    {
                        trackerEntryStatus.message = endpointStatus.message;
                    }
                }

                if (endpointStatus.state == TrackerEndpointState::Working)
                {
                    if (trackerEntryStatus.message.isEmpty())
                        trackerEntryStatus.message = endpointStatus.message;
                }
            }
        }
    }

    template <typename Vector>
    Vector resized(const Vector &inVector, const typename Vector::size_type size, const typename Vector::value_type &defaultValue)
    {
        Vector outVector = inVector;
        outVector.resize(size, defaultValue);
        return outVector;
    }

    // This is an imitation of limit normalization performed by libtorrent itself.
    // We need perform it to keep cached values in line with the ones used by libtorrent.
    int cleanLimitValue(const int value)
    {
        return ((value < 0) || (value == std::numeric_limits<int>::max())) ? 0 : value;
    }
}

// TorrentImpl

TorrentImpl::TorrentImpl(SessionImpl *session, const lt::torrent_handle &nativeHandle, LoadTorrentParams params)
    : Torrent(session)
    , m_session(session)
    , m_nativeHandle(nativeHandle)
#ifdef QBT_USES_LIBTORRENT2
    , m_infoHash(m_nativeHandle.info_hashes())
#else
    , m_infoHash(m_nativeHandle.info_hash())
#endif
    , m_name(params.name)
    , m_savePath(params.savePath)
    , m_downloadPath(params.downloadPath)
    , m_category(params.category)
    , m_tags(params.tags)
    , m_ratioLimit(params.ratioLimit)
    , m_seedingTimeLimit(params.seedingTimeLimit)
    , m_inactiveSeedingTimeLimit(params.inactiveSeedingTimeLimit)
    , m_shareLimitAction(params.shareLimitAction)
    , m_operatingMode(params.operatingMode)
    , m_contentLayout(params.contentLayout)
    , m_hasFinishedStatus(params.hasFinishedStatus)
    , m_hasFirstLastPiecePriority(params.firstLastPiecePriority)
    , m_useAutoTMM(params.useAutoTMM)
    , m_isStopped(params.stopped)
    , m_sslParams(params.sslParameters)
    , m_ltAddTorrentParams(std::move(params.ltAddTorrentParams))
    , m_downloadLimit(cleanLimitValue(m_ltAddTorrentParams.download_limit))
    , m_uploadLimit(cleanLimitValue(m_ltAddTorrentParams.upload_limit))
{
    if (m_ltAddTorrentParams.ti)
    {
        if (const std::time_t creationDate = m_ltAddTorrentParams.ti->creation_date(); creationDate > 0)
            m_creationDate = QDateTime::fromSecsSinceEpoch(creationDate);
        m_creator = QString::fromStdString(m_ltAddTorrentParams.ti->creator());
        m_comment = QString::fromStdString(m_ltAddTorrentParams.ti->comment());

        // Initialize it only if torrent is added with metadata.
        // Otherwise it should be initialized in "Metadata received" handler.
        m_torrentInfo = TorrentInfo(*m_ltAddTorrentParams.ti);

        Q_ASSERT(m_filePaths.isEmpty());
        Q_ASSERT(m_indexMap.isEmpty());
        const int filesCount = m_torrentInfo.filesCount();
        m_filePaths.reserve(filesCount);
        m_indexMap.reserve(filesCount);
        m_filePriorities.reserve(filesCount);
        const std::vector<lt::download_priority_t> filePriorities =
                resized(m_ltAddTorrentParams.file_priorities, m_ltAddTorrentParams.ti->num_files()
                        , LT::toNative(m_ltAddTorrentParams.file_priorities.empty() ? DownloadPriority::Normal : DownloadPriority::Ignored));

        m_completedFiles.fill(static_cast<bool>(m_ltAddTorrentParams.flags & lt::torrent_flags::seed_mode), filesCount);
        m_filesProgress.resize(filesCount);

        for (int i = 0; i < filesCount; ++i)
        {
            const lt::file_index_t nativeIndex = m_torrentInfo.nativeIndexes().at(i);
            m_indexMap[nativeIndex] = i;

            const auto fileIter = m_ltAddTorrentParams.renamed_files.find(nativeIndex);
            const Path filePath = ((fileIter != m_ltAddTorrentParams.renamed_files.end())
                    ? makeUserPath(Path(fileIter->second)) : m_torrentInfo.filePath(i));
            m_filePaths.append(filePath);

            const auto priority = LT::fromNative(filePriorities[LT::toUnderlyingType(nativeIndex)]);
            m_filePriorities.append(priority);
        }
    }

    if (!params.comment.isEmpty())
        m_comment = params.comment;

    setStopCondition(params.stopCondition);

    const auto *extensionData = static_cast<ExtensionData *>(m_ltAddTorrentParams.userdata);
    m_trackerEntryStatuses.reserve(static_cast<decltype(m_trackerEntryStatuses)::size_type>(extensionData->trackers.size()));
    for (const lt::announce_entry &announceEntry : extensionData->trackers)
        m_trackerEntryStatuses.append({QString::fromStdString(announceEntry.url), announceEntry.tier});
    m_urlSeeds.reserve(static_cast<decltype(m_urlSeeds)::size_type>(extensionData->urlSeeds.size()));
    for (const std::string &urlSeed : extensionData->urlSeeds)
        m_urlSeeds.append(QString::fromStdString(urlSeed));
    m_nativeStatus = extensionData->status;

    m_addedTime = QDateTime::fromSecsSinceEpoch(m_nativeStatus.added_time);
    if (m_nativeStatus.completed_time > 0)
        m_completedTime = QDateTime::fromSecsSinceEpoch(m_nativeStatus.completed_time);
    if (m_nativeStatus.last_seen_complete > 0)
        m_lastSeenComplete = QDateTime::fromSecsSinceEpoch(m_nativeStatus.last_seen_complete);

    if (hasMetadata())
        updateProgress();

    updateState();

    if (hasMetadata())
        applyFirstLastPiecePriority(m_hasFirstLastPiecePriority);
}

TorrentImpl::~TorrentImpl() = default;

bool TorrentImpl::isValid() const
{
    return m_nativeHandle.is_valid();
}

Session *TorrentImpl::session() const
{
    return m_session;
}

InfoHash TorrentImpl::infoHash() const
{
    return m_infoHash;
}

QString TorrentImpl::name() const
{
    if (!m_name.isEmpty())
        return m_name;

    if (hasMetadata())
        return m_torrentInfo.name();

    const QString name = QString::fromStdString(m_nativeStatus.name);
    if (!name.isEmpty())
        return name;

    return id().toString();
}

QDateTime TorrentImpl::creationDate() const
{
    return m_creationDate;
}

QString TorrentImpl::creator() const
{
    return m_creator;
}

QString TorrentImpl::comment() const
{
    return m_comment;
}

void TorrentImpl::setComment(const QString &comment)
{
    if (m_comment != comment)
    {
        m_comment = comment;
        deferredRequestResumeData();
    }
}

bool TorrentImpl::isPrivate() const
{
    return m_torrentInfo.isPrivate();
}

qlonglong TorrentImpl::totalSize() const
{
    return m_torrentInfo.totalSize();
}

// size without the "don't download" files
qlonglong TorrentImpl::wantedSize() const
{
    return m_nativeStatus.total_wanted;
}

qlonglong TorrentImpl::completedSize() const
{
    return m_nativeStatus.total_wanted_done;
}

qlonglong TorrentImpl::pieceLength() const
{
    return m_torrentInfo.pieceLength();
}

qlonglong TorrentImpl::wastedSize() const
{
    return (m_nativeStatus.total_failed_bytes + m_nativeStatus.total_redundant_bytes);
}

QString TorrentImpl::currentTracker() const
{
    if (!m_nativeStatus.current_tracker.empty())
        return QString::fromStdString(m_nativeStatus.current_tracker);

    if (!m_trackerEntryStatuses.isEmpty())
        return m_trackerEntryStatuses.constFirst().url;

    return {};
}

Path TorrentImpl::savePath() const
{
    return isAutoTMMEnabled() ? m_session->categorySavePath(category()) : m_savePath;
}

void TorrentImpl::setSavePath(const Path &path)
{
    Q_ASSERT(!isAutoTMMEnabled());
    if (isAutoTMMEnabled()) [[unlikely]]
        return;

    const Path basePath = m_session->useCategoryPathsInManualMode()
            ? m_session->categorySavePath(category()) : m_session->savePath();
    const Path resolvedPath = (path.isAbsolute() ? path : (basePath / path));
    if (resolvedPath == savePath())
        return;

    if (isFinished() || m_hasFinishedStatus || downloadPath().isEmpty())
    {
        moveStorage(resolvedPath, MoveStorageContext::ChangeSavePath);
    }
    else
    {
        m_savePath = resolvedPath;
        m_session->handleTorrentSavePathChanged(this);
        deferredRequestResumeData();
    }
}

Path TorrentImpl::downloadPath() const
{
    return isAutoTMMEnabled() ? m_session->categoryDownloadPath(category()) : m_downloadPath;
}

void TorrentImpl::setDownloadPath(const Path &path)
{
    Q_ASSERT(!isAutoTMMEnabled());
    if (isAutoTMMEnabled()) [[unlikely]]
        return;

    const Path basePath = m_session->useCategoryPathsInManualMode()
            ? m_session->categoryDownloadPath(category()) : m_session->downloadPath();
    const Path resolvedPath = (path.isEmpty() || path.isAbsolute()) ? path : (basePath / path);
    if (resolvedPath == m_downloadPath)
        return;

    const bool isIncomplete = !(isFinished() || m_hasFinishedStatus);
    if (isIncomplete)
    {
        moveStorage((resolvedPath.isEmpty() ? savePath() : resolvedPath), MoveStorageContext::ChangeDownloadPath);
    }
    else
    {
        m_downloadPath = resolvedPath;
        m_session->handleTorrentSavePathChanged(this);
        deferredRequestResumeData();
    }
}

Path TorrentImpl::rootPath() const
{
    if (!hasMetadata())
        return {};

    const Path relativeRootPath = Path::findRootFolder(filePaths());
    if (relativeRootPath.isEmpty())
        return {};

    return (actualStorageLocation() / relativeRootPath);
}

Path TorrentImpl::contentPath() const
{
    if (!hasMetadata())
        return {};

    if (filesCount() == 1)
        return (actualStorageLocation() / filePath(0));

    const Path rootPath = this->rootPath();
    return (rootPath.isEmpty() ? actualStorageLocation() : rootPath);
}

bool TorrentImpl::isAutoTMMEnabled() const
{
    return m_useAutoTMM;
}

void TorrentImpl::setAutoTMMEnabled(bool enabled)
{
    if (m_useAutoTMM == enabled)
        return;

    m_useAutoTMM = enabled;
    if (!m_useAutoTMM)
    {
        m_savePath = m_session->categorySavePath(category());
        m_downloadPath = m_session->categoryDownloadPath(category());
    }

    deferredRequestResumeData();
    m_session->handleTorrentSavingModeChanged(this);

    adjustStorageLocation();
}

Path TorrentImpl::actualStorageLocation() const
{
    if (!hasMetadata())
        return {};

    return Path(m_nativeStatus.save_path);
}

void TorrentImpl::setAutoManaged(const bool enable)
{
    if (enable)
        m_nativeHandle.set_flags(lt::torrent_flags::auto_managed);
    else
        m_nativeHandle.unset_flags(lt::torrent_flags::auto_managed);
}

Path TorrentImpl::makeActualPath(int index, const Path &path) const
{
    Path actualPath = path;

    if (m_session->isAppendExtensionEnabled()
            && (fileSize(index) > 0) && !m_completedFiles.at(index))
    {
        actualPath += QB_EXT;
    }

    if (m_session->isUnwantedFolderEnabled()
            && (m_filePriorities[index] == DownloadPriority::Ignored))
    {
        const Path parentPath = actualPath.parentPath();
        const QString fileName = actualPath.filename();
        actualPath = parentPath / Path(UNWANTED_FOLDER_NAME) / Path(fileName);
    }

    return actualPath;
}

Path TorrentImpl::makeUserPath(const Path &path) const
{
    Path userPath = path.removedExtension(QB_EXT);

    const Path parentRelPath = userPath.parentPath();
    if (parentRelPath.filename() == UNWANTED_FOLDER_NAME)
    {
        const QString fileName = userPath.filename();
        const Path relPath = parentRelPath.parentPath();
        userPath = relPath / Path(fileName);
    }

    return userPath;
}

QList<TrackerEntryStatus> TorrentImpl::trackers() const
{
    return m_trackerEntryStatuses;
}

void TorrentImpl::addTrackers(QList<TrackerEntry> trackers)
{
    trackers.removeIf([](const TrackerEntry &trackerEntry) { return trackerEntry.url.isEmpty(); });

    QSet<TrackerEntry> currentTrackerSet;
    currentTrackerSet.reserve(m_trackerEntryStatuses.size());
    for (const TrackerEntryStatus &status : asConst(m_trackerEntryStatuses))
        currentTrackerSet.insert({.url = status.url, .tier = status.tier});

    const auto newTrackerSet = QSet<TrackerEntry>(trackers.cbegin(), trackers.cend()) - currentTrackerSet;
    if (newTrackerSet.isEmpty())
        return;

    trackers = QList<TrackerEntry>(newTrackerSet.cbegin(), newTrackerSet.cend());
    for (const TrackerEntry &tracker : asConst(trackers))
    {
        m_nativeHandle.add_tracker(makeNativeAnnounceEntry(tracker.url, tracker.tier));
        m_trackerEntryStatuses.append({tracker.url, tracker.tier});
    }
    std::ranges::sort(m_trackerEntryStatuses
        , [](const TrackerEntryStatus &left, const TrackerEntryStatus &right) { return left.tier < right.tier; });

    deferredRequestResumeData();
    m_session->handleTorrentTrackersAdded(this, trackers);
}

void TorrentImpl::removeTrackers(const QStringList &trackers)
{
    QStringList removedTrackers = trackers;
    for (const QString &tracker : trackers)
    {
        if (!m_trackerEntryStatuses.removeOne({tracker}))
            removedTrackers.removeOne(tracker);
    }

    std::vector<lt::announce_entry> nativeTrackers;
    nativeTrackers.reserve(m_trackerEntryStatuses.size());
    for (const TrackerEntryStatus &tracker : asConst(m_trackerEntryStatuses))
        nativeTrackers.emplace_back(makeNativeAnnounceEntry(tracker.url, tracker.tier));

    if (!removedTrackers.isEmpty())
    {
        m_nativeHandle.replace_trackers(nativeTrackers);
        m_announceStatus.reset();

        deferredRequestResumeData();
        m_session->handleTorrentTrackersRemoved(this, removedTrackers);
    }
}

void TorrentImpl::replaceTrackers(QList<TrackerEntry> trackers)
{
    trackers.removeIf([](const TrackerEntry &trackerEntry) { return trackerEntry.url.isEmpty(); });

    // Filter out duplicate trackers
    const auto uniqueTrackers = QSet<TrackerEntry>(trackers.cbegin(), trackers.cend());
    trackers = QList<TrackerEntry>(uniqueTrackers.cbegin(), uniqueTrackers.cend());
    std::ranges::sort(trackers
        , [](const TrackerEntry &left, const TrackerEntry &right) { return left.tier < right.tier; });

    std::vector<lt::announce_entry> nativeTrackers;
    nativeTrackers.reserve(trackers.size());
    const auto oldEntries = std::exchange(m_trackerEntryStatuses, {});

    for (const TrackerEntry &tracker : asConst(trackers))
    {
        nativeTrackers.emplace_back(makeNativeAnnounceEntry(tracker.url, tracker.tier));
        m_trackerEntryStatuses.append({tracker.url, tracker.tier});
    }

    m_nativeHandle.replace_trackers(nativeTrackers);
    m_announceStatus.reset();

    // Clear the peer list if it's a private torrent since
    // we do not want to keep connecting with peers from old tracker.
    if (isPrivate())
        clearPeers();

    deferredRequestResumeData();
    m_session->handleTorrentTrackersReset(this, oldEntries, trackers);
}

QList<QUrl> TorrentImpl::urlSeeds() const
{
    return m_urlSeeds;
}

void TorrentImpl::addUrlSeeds(const QList<QUrl> &urlSeeds)
{
    m_session->invokeAsync([urlSeeds, session = m_session
                           , nativeHandle = m_nativeHandle
                           , thisTorrent = QPointer<TorrentImpl>(this)]
    {
        try
        {
            const std::set<std::string> nativeSeeds = nativeHandle.url_seeds();
            QList<QUrl> currentSeeds;
            currentSeeds.reserve(static_cast<decltype(currentSeeds)::size_type>(nativeSeeds.size()));
            for (const std::string &urlSeed : nativeSeeds)
                currentSeeds.append(QString::fromStdString(urlSeed));

            QList<QUrl> addedUrlSeeds;
            addedUrlSeeds.reserve(urlSeeds.size());

            for (const QUrl &url : urlSeeds)
            {
                if (!currentSeeds.contains(url))
                {
                    nativeHandle.add_url_seed(url.toString().toStdString());
                    addedUrlSeeds.append(url);
                }
            }

            currentSeeds.append(addedUrlSeeds);
            session->invoke([session, thisTorrent, currentSeeds, addedUrlSeeds]
            {
                if (!thisTorrent)
                    return;

                thisTorrent->m_urlSeeds = currentSeeds;
                if (!addedUrlSeeds.isEmpty())
                {
                    thisTorrent->deferredRequestResumeData();
                    session->handleTorrentUrlSeedsAdded(thisTorrent, addedUrlSeeds);
                }
            });
        }
        catch (const std::exception &) {}
    });
}

void TorrentImpl::removeUrlSeeds(const QList<QUrl> &urlSeeds)
{
    m_session->invokeAsync([urlSeeds, session = m_session
                           , nativeHandle = m_nativeHandle
                           , thisTorrent = QPointer<TorrentImpl>(this)]
    {
        try
        {
            const std::set<std::string> nativeSeeds = nativeHandle.url_seeds();
            QList<QUrl> currentSeeds;
            currentSeeds.reserve(static_cast<decltype(currentSeeds)::size_type>(nativeSeeds.size()));
            for (const std::string &urlSeed : nativeSeeds)
                currentSeeds.append(QString::fromStdString(urlSeed));

            QList<QUrl> removedUrlSeeds;
            removedUrlSeeds.reserve(urlSeeds.size());

            for (const QUrl &url : urlSeeds)
            {
                if (currentSeeds.removeOne(url))
                {
                    nativeHandle.remove_url_seed(url.toString().toStdString());
                    removedUrlSeeds.append(url);
                }
            }

            session->invoke([session, thisTorrent, currentSeeds, removedUrlSeeds]
            {
                if (!thisTorrent)
                    return;

                thisTorrent->m_urlSeeds = currentSeeds;

                if (!removedUrlSeeds.isEmpty())
                {
                    thisTorrent->deferredRequestResumeData();
                    session->handleTorrentUrlSeedsRemoved(thisTorrent, removedUrlSeeds);
                }
            });
        }
        catch (const std::exception &) {}
    });
}

void TorrentImpl::clearPeers()
{
    m_nativeHandle.clear_peers();
}

bool TorrentImpl::connectPeer(const PeerAddress &peerAddress)
{
    lt::error_code ec;
    const lt::address addr = lt::make_address(peerAddress.ip.toString().toStdString(), ec);
    if (ec) return false;

    const lt::tcp::endpoint endpoint(addr, peerAddress.port);
    try
    {
        m_nativeHandle.connect_peer(endpoint);
    }
    catch (const lt::system_error &err)
    {
        LogMsg(tr("Failed to add peer \"%1\" to torrent \"%2\". Reason: %3")
            .arg(peerAddress.toString(), name(), QString::fromLocal8Bit(err.what())), Log::WARNING);
        return false;
    }

    LogMsg(tr("Peer \"%1\" is added to torrent \"%2\"").arg(peerAddress.toString(), name()));
    return true;
}

bool TorrentImpl::needSaveResumeData() const
{
    return m_nativeStatus.need_save_resume;
}

void TorrentImpl::requestResumeData(const lt::resume_data_flags_t flags)
{
    m_nativeHandle.save_resume_data(flags);
    m_deferredRequestResumeDataInvoked = false;

    m_session->handleTorrentResumeDataRequested(this);
}

void TorrentImpl::deferredRequestResumeData()
{
    if (!m_deferredRequestResumeDataInvoked)
    {
        QMetaObject::invokeMethod(this, [this]
        {
            requestResumeData((m_maintenanceJob == MaintenanceJob::HandleMetadata)
                    ? lt::torrent_handle::save_info_dict : lt::resume_data_flags_t());
        }, Qt::QueuedConnection);

        m_deferredRequestResumeDataInvoked = true;
    }
}

int TorrentImpl::filesCount() const
{
    return m_torrentInfo.filesCount();
}

int TorrentImpl::piecesCount() const
{
    return m_torrentInfo.piecesCount();
}

int TorrentImpl::piecesHave() const
{
    return m_nativeStatus.num_pieces;
}

qreal TorrentImpl::progress() const
{
    if (isChecking())
        return m_nativeStatus.progress;

    if (m_nativeStatus.total_wanted == 0)
        return 0.;

    if (m_nativeStatus.total_wanted_done == m_nativeStatus.total_wanted)
        return 1.;

    const qreal progress = static_cast<qreal>(m_nativeStatus.total_wanted_done) / m_nativeStatus.total_wanted;
    if ((progress < 0.f) || (progress > 1.f))
    {
        LogMsg(tr("Unexpected data detected. Torrent: %1. Data: total_wanted=%2 total_wanted_done=%3.")
                .arg(name(), QString::number(m_nativeStatus.total_wanted), QString::number(m_nativeStatus.total_wanted_done))
                , Log::WARNING);
    }

    return progress;
}

QString TorrentImpl::category() const
{
    return m_category;
}

bool TorrentImpl::belongsToCategory(const QString &category) const
{
    if (m_category.isEmpty())
        return category.isEmpty();

    if (m_category == category)
        return true;

    return m_category.startsWith(category + u'/');
}

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

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

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

    if (!m_session->hasTag(tag))
    {
        if (!m_session->addTag(tag))
            return false;
    }
    m_tags.insert(tag);
    deferredRequestResumeData();
    m_session->handleTorrentTagAdded(this, tag);
    return true;
}

bool TorrentImpl::removeTag(const Tag &tag)
{
    if (m_tags.remove(tag))
    {
        deferredRequestResumeData();
        m_session->handleTorrentTagRemoved(this, tag);
        return true;
    }
    return false;
}

void TorrentImpl::removeAllTags()
{
    for (const Tag &tag : asConst(tags()))
        removeTag(tag);
}

QDateTime TorrentImpl::addedTime() const
{
    return m_addedTime;
}

QDateTime TorrentImpl::completedTime() const
{
    return m_completedTime;
}

QDateTime TorrentImpl::lastSeenComplete() const
{
    return m_lastSeenComplete;
}

qlonglong TorrentImpl::activeTime() const
{
    return lt::total_seconds(m_nativeStatus.active_duration);
}

qlonglong TorrentImpl::finishedTime() const
{
    return lt::total_seconds(m_nativeStatus.finished_duration);
}

qlonglong TorrentImpl::timeSinceUpload() const
{
    if (m_nativeStatus.last_upload.time_since_epoch().count() == 0)
        return -1;

    return lt::total_seconds(lt::clock_type::now() - m_nativeStatus.last_upload);
}

qlonglong TorrentImpl::timeSinceDownload() const
{
    if (m_nativeStatus.last_download.time_since_epoch().count() == 0)
        return -1;

    return lt::total_seconds(lt::clock_type::now() - m_nativeStatus.last_download);
}

qlonglong TorrentImpl::timeSinceActivity() const
{
    const qlonglong upTime = timeSinceUpload();
    const qlonglong downTime = timeSinceDownload();
    return ((upTime < 0) != (downTime < 0))
               ? std::max(upTime, downTime)
               : std::min(upTime, downTime);
}

qreal TorrentImpl::ratioLimit() const
{
    return m_ratioLimit;
}

int TorrentImpl::seedingTimeLimit() const
{
    return m_seedingTimeLimit;
}

int TorrentImpl::inactiveSeedingTimeLimit() const
{
    return m_inactiveSeedingTimeLimit;
}

Path TorrentImpl::filePath(const int index) const
{
    Q_ASSERT(index >= 0);
    Q_ASSERT(index < m_filePaths.size());

    return m_filePaths.value(index, {});
}

Path TorrentImpl::actualFilePath(const int index) const
{
    const QList<lt::file_index_t> nativeIndexes = m_torrentInfo.nativeIndexes();

    Q_ASSERT(index >= 0);
    Q_ASSERT(index < nativeIndexes.size());
    if ((index < 0) || (index >= nativeIndexes.size()))
        return {};

    return Path(nativeTorrentInfo()->files().file_path(nativeIndexes[index]));
}

qlonglong TorrentImpl::fileSize(const int index) const
{
    return m_torrentInfo.fileSize(index);
}

PathList TorrentImpl::filePaths() const
{
    return m_filePaths;
}

PathList TorrentImpl::actualFilePaths() const
{
    if (!hasMetadata())
        return {};

    PathList paths;
    paths.reserve(filesCount());

    const lt::file_storage files = nativeTorrentInfo()->files();
    for (const lt::file_index_t &nativeIndex : asConst(m_torrentInfo.nativeIndexes()))
        paths.emplaceBack(files.file_path(nativeIndex));

    return paths;
}

QList<DownloadPriority> TorrentImpl::filePriorities() const
{
    return m_filePriorities;
}

TorrentInfo TorrentImpl::info() const
{
    return m_torrentInfo;
}

bool TorrentImpl::isStopped() const
{
    return m_isStopped;
}

bool TorrentImpl::isQueued() const
{
    if (!m_session->isQueueingSystemEnabled())
        return false;

    // Torrent is Queued if it isn't in Stopped state but paused internally
    return (!isStopped()
            && (m_nativeStatus.flags & lt::torrent_flags::auto_managed)
            && (m_nativeStatus.flags & lt::torrent_flags::paused));
}

bool TorrentImpl::isChecking() const
{
    return ((m_nativeStatus.state == lt::torrent_status::checking_files)
            || (m_nativeStatus.state == lt::torrent_status::checking_resume_data));
}

bool TorrentImpl::isDownloading() const
{
    switch (m_state)
    {
    case TorrentState::Downloading:
    case TorrentState::DownloadingMetadata:
    case TorrentState::ForcedDownloadingMetadata:
    case TorrentState::StalledDownloading:
    case TorrentState::CheckingDownloading:
    case TorrentState::StoppedDownloading:
    case TorrentState::QueuedDownloading:
    case TorrentState::ForcedDownloading:
        return true;
    default:
        break;
    };

    return false;
}

bool TorrentImpl::isMoving() const
{
    return m_state == TorrentState::Moving;
}

bool TorrentImpl::isUploading() const
{
    switch (m_state)
    {
    case TorrentState::Uploading:
    case TorrentState::StalledUploading:
    case TorrentState::CheckingUploading:
    case TorrentState::QueuedUploading:
    case TorrentState::ForcedUploading:
        return true;
    default:
        break;
    };

    return false;
}

bool TorrentImpl::isCompleted() const
{
    switch (m_state)
    {
    case TorrentState::Uploading:
    case TorrentState::StalledUploading:
    case TorrentState::CheckingUploading:
    case TorrentState::StoppedUploading:
    case TorrentState::QueuedUploading:
    case TorrentState::ForcedUploading:
        return true;
    default:
        break;
    };

    return false;
}

bool TorrentImpl::isActive() const
{
    return ((uploadPayloadRate() > 0) || (downloadPayloadRate() > 0));
}

bool TorrentImpl::isInactive() const
{
    return !isActive();
}

bool TorrentImpl::isErrored() const
{
    return ((m_state == TorrentState::MissingFiles)
            || (m_state == TorrentState::Error));
}

bool TorrentImpl::isFinished() const
{
    return ((m_nativeStatus.state == lt::torrent_status::finished)
            || (m_nativeStatus.state == lt::torrent_status::seeding));
}

bool TorrentImpl::isForced() const
{
    return (!isStopped() && (m_operatingMode == TorrentOperatingMode::Forced));
}

bool TorrentImpl::isSequentialDownload() const
{
    return static_cast<bool>(m_nativeStatus.flags & lt::torrent_flags::sequential_download);
}

bool TorrentImpl::hasFirstLastPiecePriority() const
{
    return m_hasFirstLastPiecePriority;
}

TorrentState TorrentImpl::state() const
{
    return m_state;
}

void TorrentImpl::updateState()
{
    if (m_nativeStatus.state == lt::torrent_status::checking_resume_data)
    {
        m_state = TorrentState::CheckingResumeData;
    }
    else if (isMoveInProgress())
    {
        m_state = TorrentState::Moving;
    }
    else if (hasMissingFiles())
    {
        m_state = TorrentState::MissingFiles;
    }
    else if (hasError())
    {
        m_state = TorrentState::Error;
    }
    else if (!hasMetadata())
    {
        if (isStopped())
            m_state = TorrentState::StoppedDownloading;
        else if (isQueued())
            m_state = TorrentState::QueuedDownloading;
        else
            m_state = isForced() ? TorrentState::ForcedDownloadingMetadata : TorrentState::DownloadingMetadata;
    }
    else if ((m_nativeStatus.state == lt::torrent_status::checking_files) && !isStopped())
    {
        // If the torrent is not just in the "checking" state, but is being actually checked
        m_state = m_hasFinishedStatus ? TorrentState::CheckingUploading : TorrentState::CheckingDownloading;
    }
    else if (isFinished())
    {
        if (isStopped())
            m_state = TorrentState::StoppedUploading;
        else if (isQueued())
            m_state = TorrentState::QueuedUploading;
        else if (isForced())
            m_state = TorrentState::ForcedUploading;
        else if (m_nativeStatus.upload_payload_rate > 0)
            m_state = TorrentState::Uploading;
        else
            m_state = TorrentState::StalledUploading;
    }
    else
    {
        if (isStopped())
            m_state = TorrentState::StoppedDownloading;
        else if (isQueued())
            m_state = TorrentState::QueuedDownloading;
        else if (isForced())
            m_state = TorrentState::ForcedDownloading;
        else if (m_nativeStatus.download_payload_rate > 0)
            m_state = TorrentState::Downloading;
        else
            m_state = TorrentState::StalledDownloading;
    }
}

bool TorrentImpl::hasMetadata() const
{
    return m_torrentInfo.isValid();
}

bool TorrentImpl::hasMissingFiles() const
{
    return m_hasMissingFiles;
}

bool TorrentImpl::hasError() const
{
    return (m_nativeStatus.errc || (m_nativeStatus.flags & lt::torrent_flags::upload_mode));
}

int TorrentImpl::queuePosition() const
{
    return static_cast<int>(m_nativeStatus.queue_position);
}

QString TorrentImpl::error() const
{
    if (m_nativeStatus.errc)
        return Utils::String::fromLocal8Bit(m_nativeStatus.errc.message());

    if (m_nativeStatus.flags & lt::torrent_flags::upload_mode)
    {
        return tr("Couldn't write to file. Reason: \"%1\". Torrent is now in \"upload only\" mode.")
            .arg(Utils::String::fromLocal8Bit(m_lastFileError.error.message()));
    }

    return {};
}

qlonglong TorrentImpl::totalDownload() const
{
    return m_nativeStatus.all_time_download;
}

qlonglong TorrentImpl::totalUpload() const
{
    return m_nativeStatus.all_time_upload;
}

qlonglong TorrentImpl::eta() const
{
    if (isStopped()) return MAX_ETA;

    const SpeedSampleAvg speedAverage = m_payloadRateMonitor.average();

    if (isFinished())
    {
        const qreal maxRatioValue = effectiveRatioLimit();
        const int maxSeedingTimeValue = effectiveSeedingTimeLimit();
        const int maxInactiveSeedingTimeValue = effectiveInactiveSeedingTimeLimit();
        if ((maxRatioValue < 0) && (maxSeedingTimeValue < 0) && (maxInactiveSeedingTimeValue < 0))
            return MAX_ETA;

        qlonglong ratioEta = MAX_ETA;

        if ((speedAverage.upload > 0) && (maxRatioValue >= 0))
        {
            qlonglong realDL = totalDownload();
            if (realDL <= 0)
                realDL = wantedSize();

            ratioEta = ((realDL * maxRatioValue) - totalUpload()) / speedAverage.upload;
        }

        qlonglong seedingTimeEta = MAX_ETA;

        if (maxSeedingTimeValue >= 0)
        {
            seedingTimeEta = (maxSeedingTimeValue * 60) - finishedTime();
            if (seedingTimeEta < 0)
                seedingTimeEta = 0;
        }

        qlonglong inactiveSeedingTimeEta = MAX_ETA;

        if (maxInactiveSeedingTimeValue >= 0)
        {
            inactiveSeedingTimeEta = (maxInactiveSeedingTimeValue * 60) - timeSinceActivity();
            inactiveSeedingTimeEta = std::max<qlonglong>(inactiveSeedingTimeEta, 0);
        }

        return std::min({ratioEta, seedingTimeEta, inactiveSeedingTimeEta});
    }

    if (!speedAverage.download) return MAX_ETA;

    return (wantedSize() - completedSize()) / speedAverage.download;
}

QList<qreal> TorrentImpl::filesProgress() const
{
    if (!hasMetadata())
        return {};

    const qsizetype count = m_filesProgress.size();
    Q_ASSERT(count == filesCount());
    if (count != filesCount()) [[unlikely]]
        return {};

    if (m_completedFiles.count(true) == count)
        return QList<qreal>(count, 1);

    QList<qreal> result;
    result.reserve(count);
    for (int i = 0; i < count; ++i)
    {
        const int64_t progress = m_filesProgress.at(i);
        const int64_t size = fileSize(i);
        if ((size <= 0) || (progress == size))
            result << 1;
        else
            result << (progress / static_cast<qreal>(size));
    }

    return result;
}

int TorrentImpl::seedsCount() const
{
    return m_nativeStatus.num_seeds;
}

int TorrentImpl::peersCount() const
{
    return m_nativeStatus.num_peers;
}

int TorrentImpl::leechsCount() const
{
    return (m_nativeStatus.num_peers - m_nativeStatus.num_seeds);
}

int TorrentImpl::totalSeedsCount() const
{
    return (m_nativeStatus.num_complete > -1) ? m_nativeStatus.num_complete : m_nativeStatus.list_seeds;
}

int TorrentImpl::totalPeersCount() const
{
    const int peers = m_nativeStatus.num_complete + m_nativeStatus.num_incomplete;
    return (peers > -1) ? peers : m_nativeStatus.list_peers;
}

int TorrentImpl::totalLeechersCount() const
{
    return (m_nativeStatus.num_incomplete > -1) ? m_nativeStatus.num_incomplete : (m_nativeStatus.list_peers - m_nativeStatus.list_seeds);
}

int TorrentImpl::downloadLimit() const
{
    return m_downloadLimit;
}

int TorrentImpl::uploadLimit() const
{
    return m_uploadLimit;
}

bool TorrentImpl::superSeeding() const
{
    return static_cast<bool>(m_nativeStatus.flags & lt::torrent_flags::super_seeding);
}

bool TorrentImpl::isDHTDisabled() const
{
    return static_cast<bool>(m_nativeStatus.flags & lt::torrent_flags::disable_dht);
}

bool TorrentImpl::isPEXDisabled() const
{
    return static_cast<bool>(m_nativeStatus.flags & lt::torrent_flags::disable_pex);
}

bool TorrentImpl::isLSDDisabled() const
{
    return static_cast<bool>(m_nativeStatus.flags & lt::torrent_flags::disable_lsd);
}

QBitArray TorrentImpl::pieces() const
{
    return m_pieces;
}

qreal TorrentImpl::distributedCopies() const
{
    return m_nativeStatus.distributed_copies;
}

qreal TorrentImpl::effectiveRatioLimit() const
{
    if (m_ratioLimit == DEFAULT_RATIO_LIMIT)
        return m_session->categoryRatioLimit(category());

    return m_ratioLimit;
}

int TorrentImpl::effectiveSeedingTimeLimit() const
{
    if (m_seedingTimeLimit == DEFAULT_SEEDING_TIME_LIMIT)
        return m_session->categorySeedingTimeLimit(category());

    return m_seedingTimeLimit;
}

int TorrentImpl::effectiveInactiveSeedingTimeLimit() const
{
    if (m_inactiveSeedingTimeLimit == DEFAULT_SEEDING_TIME_LIMIT)
        return m_session->categoryInactiveSeedingTimeLimit(category());

    return m_inactiveSeedingTimeLimit;
}

ShareLimitAction TorrentImpl::effectiveShareLimitAction() const
{
    if (m_shareLimitAction == ShareLimitAction::Default)
        return m_session->categoryShareLimitAction(category());

    return m_shareLimitAction;
}

qreal TorrentImpl::realRatio() const
{
    const int64_t upload = m_nativeStatus.all_time_upload;
    // special case for a seeder who lost its stats, also assume nobody will import a 99% done torrent
    const int64_t download = (m_nativeStatus.all_time_download < (m_nativeStatus.total_done * 0.01))
        ? m_nativeStatus.total_done
        : m_nativeStatus.all_time_download;

    if (download == 0)
        return (upload == 0) ? 0 : MAX_RATIO;

    const qreal ratio = upload / static_cast<qreal>(download);
    Q_ASSERT(ratio >= 0);

    return ratio;
}

int TorrentImpl::uploadPayloadRate() const
{
    // workaround: suppress the speed for Stopped state
    return isStopped() ? 0 : m_nativeStatus.upload_payload_rate;
}

int TorrentImpl::downloadPayloadRate() const
{
    // workaround: suppress the speed for Stopped state
    return isStopped() ? 0 : m_nativeStatus.download_payload_rate;
}

qlonglong TorrentImpl::totalPayloadUpload() const
{
    return m_nativeStatus.total_payload_upload;
}

qlonglong TorrentImpl::totalPayloadDownload() const
{
    return m_nativeStatus.total_payload_download;
}

int TorrentImpl::connectionsCount() const
{
    return m_nativeStatus.num_connections;
}

int TorrentImpl::connectionsLimit() const
{
    return m_nativeStatus.connections_limit;
}

qlonglong TorrentImpl::nextAnnounce() const
{
    return lt::total_seconds(m_nativeStatus.next_announce);
}

TorrentAnnounceStatus TorrentImpl::announceStatus() const
{
    if (m_announceStatus)
        return *m_announceStatus;

    TorrentAnnounceStatus announceStatus = TorrentAnnounceStatusFlag::HasNoProblem;
    for (const TrackerEntryStatus &trackerEntryStatus : asConst(m_trackerEntryStatuses))
    {
        switch (trackerEntryStatus.state)
        {
        case BitTorrent::TrackerEndpointState::Working:
            if (!announceStatus.testFlag(TorrentAnnounceStatusFlag::HasWarning))
            {
                const bool hasWarningMessage = std::ranges::any_of(trackerEntryStatus.endpoints
                        , [](const TrackerEndpointStatus &endpointEntry)
                {
                    return !endpointEntry.message.isEmpty() && (endpointEntry.state == BitTorrent::TrackerEndpointState::Working);
                });
                announceStatus.setFlag(TorrentAnnounceStatusFlag::HasWarning, hasWarningMessage);
            }
            break;

        case BitTorrent::TrackerEndpointState::NotWorking:
        case BitTorrent::TrackerEndpointState::Unreachable:
            announceStatus.setFlag(TorrentAnnounceStatusFlag::HasOtherError);
            break;

        case BitTorrent::TrackerEndpointState::TrackerError:
            announceStatus.setFlag(TorrentAnnounceStatusFlag::HasTrackerError);
            break;

        case BitTorrent::TrackerEndpointState::NotContacted:
            break;
        };
    }

    m_announceStatus = announceStatus;
    return *m_announceStatus;
}

qreal TorrentImpl::popularity() const
{
    // in order to produce floating-point numbers using `std::chrono::duration_cast`,
    // we should use `qreal` as `Rep` to define the `months` duration
    using months = std::chrono::duration<qreal, std::chrono::months::period>;
    const auto activeMonths = std::chrono::duration_cast<months>(m_nativeStatus.active_duration).count();
    return (activeMonths > 0) ? (realRatio() / activeMonths) : 0;
}

void TorrentImpl::setName(const QString &name)
{
    if (m_name != name)
    {
        m_name = name;
        deferredRequestResumeData();
        m_session->handleTorrentNameChanged(this);
    }
}

bool TorrentImpl::setCategory(const QString &category)
{
    if (m_category != category)
    {
        if (!category.isEmpty() && !m_session->categories().contains(category))
            return false;

        if (m_session->isDisableAutoTMMWhenCategoryChanged())
        {
            // This should be done before changing the category name
            // to prevent the torrent from being moved at the path of new category.
            setAutoTMMEnabled(false);
        }

        const QString oldCategory = m_category;
        m_category = category;
        deferredRequestResumeData();
        m_session->handleTorrentCategoryChanged(this, oldCategory);

        if (m_useAutoTMM)
            adjustStorageLocation();
    }

    return true;
}

void TorrentImpl::forceReannounce(const int index)
{
    m_nativeHandle.force_reannounce(0, index);
}

void TorrentImpl::forceDHTAnnounce()
{
    m_nativeHandle.force_dht_announce();
}

void TorrentImpl::forceRecheck()
{
    if (!hasMetadata())
        return;

    m_nativeHandle.force_recheck();

    // We have to force update the cached state, otherwise someone will be able to get
    // an incorrect one during the interval until the cached state is updated in a regular way.
    m_nativeStatus.state = lt::torrent_status::checking_resume_data;
    m_nativeStatus.pieces.clear_all();
    m_nativeStatus.num_pieces = 0;
    m_ltAddTorrentParams.have_pieces.clear();
    m_ltAddTorrentParams.verified_pieces.clear();
    m_ltAddTorrentParams.unfinished_pieces.clear();
    m_completedFiles.fill(false);
    m_filesProgress.fill(0);
    m_pieces.fill(false);
    m_unchecked = false;

    if (m_hasMissingFiles)
    {
        m_hasMissingFiles = false;
        if (!isStopped())
        {
            setAutoManaged(m_operatingMode == TorrentOperatingMode::AutoManaged);
            if (m_operatingMode == TorrentOperatingMode::Forced)
                m_nativeHandle.resume();
        }
    }

    if (isStopped())
    {
        // When "force recheck" is applied on Stopped torrent, we start them to perform checking
        start();
        m_stopCondition = StopCondition::FilesChecked;
    }
}

void TorrentImpl::setSequentialDownload(const bool enable)
{
    if (enable)
    {
        m_nativeHandle.set_flags(lt::torrent_flags::sequential_download);
        m_nativeStatus.flags |= lt::torrent_flags::sequential_download;  // prevent return cached value
    }
    else
    {
        m_nativeHandle.unset_flags(lt::torrent_flags::sequential_download);
        m_nativeStatus.flags &= ~lt::torrent_flags::sequential_download;  // prevent return cached value
    }

    deferredRequestResumeData();
}

void TorrentImpl::setFirstLastPiecePriority(const bool enabled)
{
    if (m_hasFirstLastPiecePriority == enabled)
        return;

    m_hasFirstLastPiecePriority = enabled;
    if (hasMetadata())
        applyFirstLastPiecePriority(enabled);

    LogMsg(tr("Download first and last piece first: %1, torrent: '%2'")
        .arg((enabled ? tr("On") : tr("Off")), name()));

    deferredRequestResumeData();
}

void TorrentImpl::applyFirstLastPiecePriority(const bool enabled)
{
    Q_ASSERT(hasMetadata());

    // Download first and last pieces first for every file in the torrent

    auto piecePriorities = std::vector<lt::download_priority_t>(m_torrentInfo.piecesCount(), LT::toNative(DownloadPriority::Ignored));

    // Updating file priorities is an async operation in libtorrent, when we just updated it and immediately query it
    // we might get the old/wrong values, so we rely on `updatedFilePrio` in this case.
    for (qsizetype fileIndex = 0; fileIndex < m_filePriorities.size(); ++fileIndex)
    {
        const DownloadPriority filePrio = m_filePriorities[fileIndex];
        if (filePrio <= DownloadPriority::Ignored)
            continue;

        // Determine the priority to set
        const lt::download_priority_t piecePrio = LT::toNative(enabled ? DownloadPriority::Maximum : filePrio);
        const TorrentInfo::PieceRange pieceRange = m_torrentInfo.filePieces(fileIndex);

        // worst case: AVI index = 1% of total file size (at the end of the file)
        const int numPieces = std::ceil(fileSize(fileIndex) * 0.01 / pieceLength());
        for (int i = 0; i < numPieces; ++i)
        {
            piecePriorities[pieceRange.first() + i] = piecePrio;
            piecePriorities[pieceRange.last() - i] = piecePrio;
        }

        const int firstPiece = pieceRange.first() + numPieces;
        const int lastPiece = pieceRange.last() - numPieces;
        for (int pieceIndex = firstPiece; pieceIndex <= lastPiece; ++pieceIndex)
            piecePriorities[pieceIndex] = LT::toNative(filePrio);
    }

    m_nativeHandle.prioritize_pieces(piecePriorities);
}

TrackerEntryStatus TorrentImpl::updateTrackerEntryStatus(const lt::announce_entry &announceEntry, const QHash<lt::tcp::endpoint, QMap<int, int>> &updateInfo)
{
    const auto it = std::ranges::find_if(m_trackerEntryStatuses
            , [&announceEntry](const TrackerEntryStatus &trackerEntryStatus)
    {
        return (trackerEntryStatus.url == QString::fromStdString(announceEntry.url));
    });

    Q_ASSERT(it != m_trackerEntryStatuses.end());
    if (it == m_trackerEntryStatuses.end()) [[unlikely]]
        return {};

#ifdef QBT_USES_LIBTORRENT2
    QSet<int> btProtocols;
    const auto &infoHashes = nativeHandle().info_hashes();
    if (infoHashes.has(lt::protocol_version::V1))
        btProtocols.insert(1);
    if (infoHashes.has(lt::protocol_version::V2))
        btProtocols.insert(2);
#else
    const QSet<int> btProtocols {1};
#endif

    ::updateTrackerEntryStatus(*it, announceEntry, btProtocols, updateInfo);
    m_announceStatus.reset();

    return *it;
}

void TorrentImpl::resetTrackerEntryStatuses()
{
    for (TrackerEntryStatus &status : m_trackerEntryStatuses)
    {
        const QString tempUrl = status.url;
        const int tempTier = status.tier;

        status.clear();
        status.url = tempUrl;
        status.tier = tempTier;
    }

    m_announceStatus = TorrentAnnounceStatusFlag::HasNoProblem;
}

std::shared_ptr<const libtorrent::torrent_info> TorrentImpl::nativeTorrentInfo() const
{
    Q_ASSERT(!m_nativeStatus.torrent_file.expired());

    return m_nativeStatus.torrent_file.lock();
}

void TorrentImpl::endReceivedMetadataHandling(const Path &savePath, const PathList &fileNames)
{
    Q_ASSERT(m_maintenanceJob == MaintenanceJob::HandleMetadata);
    if (m_maintenanceJob != MaintenanceJob::HandleMetadata) [[unlikely]]
        return;

    Q_ASSERT(m_filePaths.isEmpty());
    if (!m_filePaths.isEmpty()) [[unlikely]]
        m_filePaths.clear();

    lt::add_torrent_params &p = m_ltAddTorrentParams;

    const std::shared_ptr<lt::torrent_info> metadata = std::const_pointer_cast<lt::torrent_info>(nativeTorrentInfo());
    m_torrentInfo = TorrentInfo(*metadata);
    m_filePriorities.reserve(filesCount());
    const auto nativeIndexes = m_torrentInfo.nativeIndexes();
    p.file_priorities = resized(p.file_priorities, metadata->files().num_files()
            , LT::toNative(p.file_priorities.empty() ? DownloadPriority::Normal : DownloadPriority::Ignored));

    m_completedFiles.fill(static_cast<bool>(p.flags & lt::torrent_flags::seed_mode), filesCount());
    m_filesProgress.resize(filesCount());
    updateProgress();

    for (qsizetype i = 0; i < fileNames.size(); ++i)
    {
        const auto nativeIndex = nativeIndexes.at(i);

        const Path &actualFilePath = fileNames.at(i);
        p.renamed_files[nativeIndex] = actualFilePath.toString().toStdString();

        const Path filePath = actualFilePath.removedExtension(QB_EXT);
        m_filePaths.append(filePath);

        m_filePriorities.append(LT::fromNative(p.file_priorities[LT::toUnderlyingType(nativeIndex)]));
    }

    m_session->applyFilenameFilter(m_filePaths, m_filePriorities);
    for (qsizetype i = 0; i < m_filePriorities.size(); ++i)
        p.file_priorities[LT::toUnderlyingType(nativeIndexes[i])] = LT::toNative(m_filePriorities[i]);

    p.save_path = savePath.toString().toStdString();
    p.ti = metadata;

    if (stopCondition() == StopCondition::MetadataReceived)
    {
        m_stopCondition = StopCondition::None;

        m_isStopped = true;
        p.flags |= lt::torrent_flags::paused;
        p.flags &= ~lt::torrent_flags::auto_managed;

        m_session->handleTorrentStopped(this);
    }

    reload();

    // If first/last piece priority was specified when adding this torrent,
    // we should apply it now that we have metadata:
    if (m_hasFirstLastPiecePriority)
        applyFirstLastPiecePriority(true);

    m_maintenanceJob = MaintenanceJob::None;
    prepareResumeData(std::move(p));

    m_session->handleTorrentMetadataReceived(this);
}

void TorrentImpl::reload()
{
    try
    {
        lt::add_torrent_params p = m_ltAddTorrentParams;
        p.flags |= lt::torrent_flags::update_subscribe
                | lt::torrent_flags::override_trackers
                | lt::torrent_flags::override_web_seeds;

        if (m_isStopped)
        {
            p.flags |= lt::torrent_flags::paused;
            p.flags &= ~lt::torrent_flags::auto_managed;
        }
        else if (m_operatingMode == TorrentOperatingMode::AutoManaged)
        {
            p.flags |= (lt::torrent_flags::auto_managed | lt::torrent_flags::paused);
        }
        else
        {
            p.flags &= ~(lt::torrent_flags::auto_managed | lt::torrent_flags::paused);
        }

        const auto queuePos = m_nativeHandle.queue_position();

        m_nativeHandle = m_session->reloadTorrent(m_nativeHandle, std::move(p));
        m_nativeStatus = static_cast<ExtensionData *>(m_nativeHandle.userdata())->status;

        if (queuePos >= lt::queue_position_t {})
            m_nativeHandle.queue_position_set(queuePos);
        m_nativeStatus.queue_position = queuePos;

        m_completedFiles.fill(false);
        m_filesProgress.fill(0);
        m_pieces.fill(false);
        m_nativeStatus.pieces.clear_all();
        m_nativeStatus.num_pieces = 0;

        updateState();
    }
    catch (const lt::system_error &err)
    {
        throw RuntimeError(tr("Failed to reload torrent. Torrent: %1. Reason: %2")
                .arg(id().toString(), QString::fromLocal8Bit(err.what())));
    }
}

void TorrentImpl::stop()
{
    if (!m_isStopped)
    {
        m_stopCondition = StopCondition::None;
        m_isStopped = true;
        deferredRequestResumeData();
        m_session->handleTorrentStopped(this);
    }

    if (m_maintenanceJob == MaintenanceJob::None)
    {
        setAutoManaged(false);
        m_nativeHandle.pause();

        m_payloadRateMonitor.reset();
    }
}

void TorrentImpl::start(const TorrentOperatingMode mode)
{
    if (hasError())
    {
        m_nativeHandle.clear_error();
        m_nativeHandle.unset_flags(lt::torrent_flags::upload_mode);
    }

    m_operatingMode = mode;

    if (m_hasMissingFiles)
    {
        m_hasMissingFiles = false;
        m_isStopped = false;
        m_ltAddTorrentParams.ti = std::const_pointer_cast<lt::torrent_info>(nativeTorrentInfo());
        reload();
        return;
    }

    if (m_isStopped)
    {
        m_isStopped = false;
        deferredRequestResumeData();
        m_session->handleTorrentStarted(this);
    }

    if (m_maintenanceJob == MaintenanceJob::None)
    {
        setAutoManaged(m_operatingMode == TorrentOperatingMode::AutoManaged);
        if (m_operatingMode == TorrentOperatingMode::Forced)
            m_nativeHandle.resume();
    }
}

void TorrentImpl::moveStorage(const Path &newPath, const MoveStorageContext context)
{
    if (!hasMetadata())
    {
        if (context == MoveStorageContext::ChangeSavePath)
        {
            m_savePath = newPath;
            m_session->handleTorrentSavePathChanged(this);
        }
        else if (context == MoveStorageContext::ChangeDownloadPath)
        {
            m_downloadPath = newPath;
            m_session->handleTorrentSavePathChanged(this);
        }

        return;
    }

    const auto mode = (context == MoveStorageContext::AdjustCurrentLocation)
            ? MoveStorageMode::Overwrite : MoveStorageMode::KeepExistingFiles;
    if (m_session->addMoveTorrentStorageJob(this, newPath, mode, context))
    {
        if (!m_storageIsMoving)
        {
            m_storageIsMoving = true;
            updateState();
            m_session->handleTorrentStorageMovingStateChanged(this);
        }
    }
}

void TorrentImpl::renameFile(const int index, const Path &path)
{
    Q_ASSERT((index >= 0) && (index < filesCount()));
    if ((index < 0) || (index >= filesCount())) [[unlikely]]
        return;

    const Path targetActualPath = makeActualPath(index, path);
    doRenameFile(index, targetActualPath);
}

void TorrentImpl::handleStateUpdate(const lt::torrent_status &nativeStatus)
{
    updateStatus(nativeStatus);
}

void TorrentImpl::handleQueueingModeChanged()
{
    updateState();
}

void TorrentImpl::handleMoveStorageJobFinished(const Path &path, const MoveStorageContext context, const bool hasOutstandingJob)
{
    if (context == MoveStorageContext::ChangeSavePath)
        m_savePath = path;
    else if (context == MoveStorageContext::ChangeDownloadPath)
        m_downloadPath = path;
    m_storageIsMoving = hasOutstandingJob;
    m_nativeStatus.save_path = path.toString().toStdString();

    m_session->handleTorrentSavePathChanged(this);
    deferredRequestResumeData();

    if (!m_storageIsMoving)
    {
        updateState();
        m_session->handleTorrentStorageMovingStateChanged(this);

        if (m_hasMissingFiles)
        {
            // it can be moved to the proper location
            m_hasMissingFiles = false;
            m_ltAddTorrentParams.save_path = m_nativeStatus.save_path;
            m_ltAddTorrentParams.ti = std::const_pointer_cast<lt::torrent_info>(nativeTorrentInfo());
            reload();
        }

        while ((m_renameCount == 0) && !m_moveFinishedTriggers.isEmpty())
            std::invoke(m_moveFinishedTriggers.dequeue());
    }
}

void TorrentImpl::handleTorrentChecked()
{
    if (!hasMetadata())
    {
        // The torrent is checked due to metadata received, but we should not process
        // this event until the torrent is reloaded using the received metadata.
        return;
    }

    if (stopCondition() == StopCondition::FilesChecked)
        stop();

    m_statusUpdatedTriggers.enqueue([this]()
    {
        qDebug("\"%s\" have just finished checking.", qUtf8Printable(name()));

        if (!m_hasMissingFiles)
        {
            if ((progress() < 1.0) && (wantedSize() > 0))
                m_hasFinishedStatus = false;
            else if (progress() == 1.0)
                m_hasFinishedStatus = true;

            adjustStorageLocation();
            manageActualFilePaths();

            if (!isStopped())
            {
                // torrent is internally paused using NativeTorrentExtension after files checked
                // so we need to resume it if there is no corresponding "stop condition" set
                setAutoManaged(m_operatingMode == TorrentOperatingMode::AutoManaged);
                if (m_operatingMode == TorrentOperatingMode::Forced)
                    m_nativeHandle.resume();
            }
        }

        if (m_nativeStatus.need_save_resume)
            deferredRequestResumeData();

        m_session->handleTorrentChecked(this);
    });
}

void TorrentImpl::handleTorrentFinished()
{
    m_hasMissingFiles = false;
    if (m_hasFinishedStatus)
        return;

    m_statusUpdatedTriggers.enqueue([this]()
    {
        adjustStorageLocation();
        manageActualFilePaths();

        deferredRequestResumeData();

        const bool recheckTorrentsOnCompletion = Preferences::instance()->recheckTorrentsOnCompletion();
        if (recheckTorrentsOnCompletion && m_unchecked)
        {
            forceRecheck();
        }
        else
        {
            m_hasFinishedStatus = true;

            if (isMoveInProgress() || (m_renameCount > 0))
                m_moveFinishedTriggers.enqueue([this] { m_session->handleTorrentFinished(this); });
            else
                m_session->handleTorrentFinished(this);
        }
    });
}

void TorrentImpl::handleSaveResumeData(lt::add_torrent_params params)
{
    if (m_ltAddTorrentParams.url_seeds != params.url_seeds)
    {
        // URL seed list have been changed by libtorrent for some reason, so we need to update cached one.
        // Unfortunately, URL seed list containing in "resume data" is generated according to different rules
        // than the list we usually cache, so we have to request it from the appropriate source.
        fetchURLSeeds().then(this, [this](const QList<QUrl> &urlSeeds) { m_urlSeeds = urlSeeds; });
    }

    if ((m_maintenanceJob == MaintenanceJob::HandleMetadata) && params.ti)
    {
        Q_ASSERT(m_indexMap.isEmpty());

        const auto isSeedMode = static_cast<bool>(m_ltAddTorrentParams.flags & lt::torrent_flags::seed_mode);
        m_ltAddTorrentParams = std::move(params);
        if (isSeedMode)
            m_ltAddTorrentParams.flags |= lt::torrent_flags::seed_mode;

        m_ltAddTorrentParams.have_pieces.clear();
        m_ltAddTorrentParams.verified_pieces.clear();
        m_ltAddTorrentParams.unfinished_pieces.clear();

        m_nativeStatus.torrent_file = m_ltAddTorrentParams.ti;

        const auto metadata = TorrentInfo(*m_ltAddTorrentParams.ti);

        const auto &renamedFiles = m_ltAddTorrentParams.renamed_files;
        PathList filePaths = metadata.filePaths();
        if (renamedFiles.empty() && (m_contentLayout != TorrentContentLayout::Original))
        {
            const Path originalRootFolder = Path::findRootFolder(filePaths);
            const auto originalContentLayout = (originalRootFolder.isEmpty()
                    ? TorrentContentLayout::NoSubfolder : TorrentContentLayout::Subfolder);
            if (m_contentLayout != originalContentLayout)
            {
                if (m_contentLayout == TorrentContentLayout::NoSubfolder)
                    Path::stripRootFolder(filePaths);
                else
                    Path::addRootFolder(filePaths, filePaths.at(0).removedExtension());
            }
        }

        const auto nativeIndexes = metadata.nativeIndexes();
        m_indexMap.reserve(filePaths.size());
        for (qsizetype i = 0; i < filePaths.size(); ++i)
        {
            const auto nativeIndex = nativeIndexes.at(i);
            m_indexMap[nativeIndex] = i;

            if (const auto it = renamedFiles.find(nativeIndex); it != renamedFiles.cend())
                filePaths[i] = Path(it->second);
        }

        m_session->findIncompleteFiles(savePath(), downloadPath(), filePaths).then(this
                , [this](const FileSearchResult &result)
        {
            if (m_maintenanceJob == MaintenanceJob::HandleMetadata)
                endReceivedMetadataHandling(result.savePath, result.fileNames);
        });
    }
    else
    {
        prepareResumeData(std::move(params));
    }
}

void TorrentImpl::prepareResumeData(lt::add_torrent_params params)
{
    {
        decltype(params.have_pieces) havePieces;
        decltype(params.unfinished_pieces) unfinishedPieces;
        decltype(params.verified_pieces) verifiedPieces;

        // The resume data obtained from libtorrent contains an empty "progress" in the following cases:
        //   1. when it was requested at a time when the initial resume data has not yet been checked,
        //   2. when initial resume data was rejected
        // We should preserve the initial "progress" in such cases.
        const bool needPreserveProgress = m_hasMissingFiles
                || (!m_ltAddTorrentParams.have_pieces.empty() && params.have_pieces.empty());
        const bool preserveSeedMode = !m_hasMissingFiles && !hasMetadata()
                && (m_ltAddTorrentParams.flags & lt::torrent_flags::seed_mode);

        if (needPreserveProgress)
        {
            havePieces = std::move(m_ltAddTorrentParams.have_pieces);
            unfinishedPieces = std::move(m_ltAddTorrentParams.unfinished_pieces);
            verifiedPieces = std::move(m_ltAddTorrentParams.verified_pieces);
        }

        // Update recent resume data
        m_ltAddTorrentParams = std::move(params);

        if (needPreserveProgress)
        {
            m_ltAddTorrentParams.have_pieces = std::move(havePieces);
            m_ltAddTorrentParams.unfinished_pieces = std::move(unfinishedPieces);
            m_ltAddTorrentParams.verified_pieces = std::move(verifiedPieces);
        }

        if (preserveSeedMode)
            m_ltAddTorrentParams.flags |= lt::torrent_flags::seed_mode;
    }

    // We shouldn't save upload_mode flag to allow torrent operate normally on next run
    m_ltAddTorrentParams.flags &= ~lt::torrent_flags::upload_mode;

    LoadTorrentParams resumeData
    {
        .ltAddTorrentParams = m_ltAddTorrentParams,
        .name = m_name,
        .category = m_category,
        .tags = m_tags,
        .savePath = (!m_useAutoTMM ? m_savePath : Path()),
        .downloadPath = (!m_useAutoTMM ? m_downloadPath : Path()),
        .comment = m_comment,
        .contentLayout = m_contentLayout,
        .operatingMode = m_operatingMode,
        .useAutoTMM = m_useAutoTMM,
        .firstLastPiecePriority = m_hasFirstLastPiecePriority,
        .hasFinishedStatus = m_hasFinishedStatus,
        .stopped = m_isStopped,
        .stopCondition = m_stopCondition,
        .addToQueueTop = false,
        .ratioLimit = m_ratioLimit,
        .seedingTimeLimit = m_seedingTimeLimit,
        .inactiveSeedingTimeLimit = m_inactiveSeedingTimeLimit,
        .shareLimitAction = m_shareLimitAction,
        .sslParameters = m_sslParams
    };

    m_session->handleTorrentResumeDataReady(this, std::move(resumeData));
}

void TorrentImpl::handleFastResumeRejected()
{
    // Files were probably moved or storage isn't accessible
    m_hasMissingFiles = true;
}

void TorrentImpl::handleFileRenamed(const lt::file_index_t nativeFileIndex, const Path &newActualFilePath, const Path &oldActualFilePath)
{
    const int fileIndex = fileIndexFromNative(nativeFileIndex);
    Q_ASSERT(fileIndex >= 0);

    const Path oldFilePath = m_filePaths.at(fileIndex);
    const Path newFilePath = makeUserPath(newActualFilePath);

    // Check if ".!qB" extension or ".unwanted" folder was just added or removed
    // We should compare path in a case sensitive manner even on case insensitive
    // platforms since it can be renamed by only changing case of some character(s)
    if (oldFilePath.data() == newFilePath.data())
    {
        // Remove empty ".unwanted" folders
        const Path oldActualParentPath = oldActualFilePath.parentPath();
        const Path newActualParentPath = newActualFilePath.parentPath();
        if (newActualParentPath.filename() == UNWANTED_FOLDER_NAME)
        {
            if (oldActualParentPath.filename() != UNWANTED_FOLDER_NAME)
            {
#ifdef Q_OS_WIN
                const std::wstring winPath = (actualStorageLocation() / newActualParentPath).toString().toStdWString();
                const DWORD dwAttrs = ::GetFileAttributesW(winPath.c_str());
                ::SetFileAttributesW(winPath.c_str(), (dwAttrs | FILE_ATTRIBUTE_HIDDEN));
#endif
            }
        }
#ifdef QBT_USES_LIBTORRENT2
        else if (oldActualParentPath.filename() == UNWANTED_FOLDER_NAME)
        {
            if (newActualParentPath.filename() != UNWANTED_FOLDER_NAME)
                Utils::Fs::rmdir(actualStorageLocation() / oldActualParentPath);
        }
#else
        else
        {
            Utils::Fs::rmdir(actualStorageLocation() / newActualParentPath / Path(UNWANTED_FOLDER_NAME));
        }
#endif
    }
    else
    {
        m_filePaths[fileIndex] = newFilePath;

        // Remove empty leftover folders
        // For example renaming "a/b/c" to "d/b/c", then folders "a/b" and "a" will
        // be removed if they are empty
        Path oldParentPath = oldFilePath.parentPath();
        const Path commonBasePath = Path::commonPath(oldParentPath, newFilePath.parentPath());
        while (oldParentPath != commonBasePath)
        {
            Utils::Fs::rmdir(actualStorageLocation() / oldParentPath);
            oldParentPath = oldParentPath.parentPath();
        }
    }

    --m_renameCount;
    while (!isMoveInProgress() && (m_renameCount == 0) && !m_moveFinishedTriggers.isEmpty())
        m_moveFinishedTriggers.takeFirst()();

    deferredRequestResumeData();
}

void TorrentImpl::handleFileRenameFailed(const lt::file_index_t nativeFileIndex)
{
    const int fileIndex = fileIndexFromNative(nativeFileIndex);
    Q_ASSERT(fileIndex >= 0);

    --m_renameCount;
    while (!isMoveInProgress() && (m_renameCount == 0) && !m_moveFinishedTriggers.isEmpty())
        m_moveFinishedTriggers.takeFirst()();

    deferredRequestResumeData();
}

void TorrentImpl::handleFileCompleted(const lt::file_index_t nativeFileIndex)
{
    if (m_maintenanceJob == MaintenanceJob::HandleMetadata)
        return;

    const int fileIndex = fileIndexFromNative(nativeFileIndex);
    Q_ASSERT(fileIndex >= 0);

    m_completedFiles.setBit(fileIndex);

    const Path actualPath = actualFilePath(fileIndex);

#if defined(Q_OS_MACOS) || defined(Q_OS_WIN)
    // only apply Mark-of-the-Web to new download files
    if (Preferences::instance()->isMarkOfTheWebEnabled()
        && (m_nativeStatus.state == lt::torrent_status::downloading))
    {
        const Path fullpath = actualStorageLocation() / actualPath;
        Utils::OS::applyMarkOfTheWeb(fullpath);
    }
#endif // Q_OS_MACOS || Q_OS_WIN

    if (m_session->isAppendExtensionEnabled())
    {
        const Path path = filePath(fileIndex);
        if (actualPath != path)
        {
            qDebug("Renaming %s to %s", qUtf8Printable(actualPath.toString()), qUtf8Printable(path.toString()));
            doRenameFile(fileIndex, path);
        }
    }
}

void TorrentImpl::handleFileError(FileErrorInfo fileError)
{
    m_lastFileError = std::move(fileError);
}

void TorrentImpl::handleMetadataReceived()
{
#ifdef QBT_USES_LIBTORRENT2
    const InfoHash prevInfoHash = infoHash();
    m_infoHash = InfoHash(m_nativeHandle.info_hashes());
    if (prevInfoHash != infoHash())
        m_session->handleTorrentInfoHashChanged(this, prevInfoHash);
#endif

    m_maintenanceJob = MaintenanceJob::HandleMetadata;
    deferredRequestResumeData();
}

void TorrentImpl::handleCategoryOptionsChanged()
{
    if (m_useAutoTMM)
        adjustStorageLocation();
}

void TorrentImpl::handleAppendExtensionToggled()
{
    if (!hasMetadata())
        return;

    manageActualFilePaths();
}

void TorrentImpl::handleUnwantedFolderToggled()
{
    if (!hasMetadata())
        return;

    manageActualFilePaths();
}

void TorrentImpl::manageActualFilePaths()
{
    const std::shared_ptr<const lt::torrent_info> nativeInfo = nativeTorrentInfo();
    const lt::file_storage &nativeFiles = nativeInfo->files();

    for (int i = 0; i < filesCount(); ++i)
    {
        const Path path = filePath(i);

        const auto nativeIndex = m_torrentInfo.nativeIndexes().at(i);
        const Path actualPath {nativeFiles.file_path(nativeIndex)};
        const Path targetActualPath = makeActualPath(i, path);
        if (actualPath != targetActualPath)
        {
            qDebug() << "Renaming" << actualPath.toString() << "to" << targetActualPath.toString();
            doRenameFile(i, targetActualPath);
        }
    }
}

void TorrentImpl::adjustStorageLocation()
{
    const Path downloadPath = this->downloadPath();
    const Path targetPath = ((isFinished() || m_hasFinishedStatus || downloadPath.isEmpty()) ? savePath() : downloadPath);

    if ((targetPath != actualStorageLocation()) || isMoveInProgress())
        moveStorage(targetPath, MoveStorageContext::AdjustCurrentLocation);
}

void TorrentImpl::doRenameFile(const int index, const Path &path)
{
    const QList<lt::file_index_t> nativeIndexes = m_torrentInfo.nativeIndexes();

    Q_ASSERT(index >= 0);
    Q_ASSERT(index < nativeIndexes.size());
    if ((index < 0) || (index >= nativeIndexes.size())) [[unlikely]]
        return;

    ++m_renameCount;
    m_nativeHandle.rename_file(nativeIndexes[index], path.toString().toStdString());
}

lt::torrent_handle TorrentImpl::nativeHandle() const
{
    return m_nativeHandle;
}

int TorrentImpl::fileIndexFromNative(const lt::file_index_t nativeFileIndex) const
{
    return m_indexMap.value(nativeFileIndex, -1);
}

void TorrentImpl::setMetadata(const TorrentInfo &torrentInfo)
{
    if (hasMetadata())
        return;

    m_session->invokeAsync([nativeHandle = m_nativeHandle, torrentInfo]
    {
        try
        {
#ifdef QBT_USES_LIBTORRENT2
            nativeHandle.set_metadata(torrentInfo.nativeInfo()->info_section());
#else
            const std::shared_ptr<lt::torrent_info> nativeInfo = torrentInfo.nativeInfo();
            nativeHandle.set_metadata(lt::span<const char>(nativeInfo->metadata().get(), nativeInfo->metadata_size()));
#endif
        }
        catch (const std::exception &) {}
    });
}

Torrent::StopCondition TorrentImpl::stopCondition() const
{
    return m_stopCondition;
}

void TorrentImpl::setStopCondition(const StopCondition stopCondition)
{
    if (stopCondition == m_stopCondition)
        return;

    if (isStopped())
        return;

    if ((stopCondition == StopCondition::MetadataReceived) && hasMetadata())
        return;

    if ((stopCondition == StopCondition::FilesChecked) && hasMetadata() && !isChecking())
        return;

    m_stopCondition = stopCondition;
}

SSLParameters TorrentImpl::getSSLParameters() const
{
    return m_sslParams;
}

void TorrentImpl::setSSLParameters(const SSLParameters &sslParams)
{
    if (sslParams == getSSLParameters())
        return;

    m_sslParams = sslParams;
    applySSLParameters();

    deferredRequestResumeData();
}

bool TorrentImpl::applySSLParameters()
{
    if (!m_sslParams.isValid())
        return false;

    m_nativeHandle.set_ssl_certificate_buffer(m_sslParams.certificate.toPem().toStdString()
        , m_sslParams.privateKey.toPem().toStdString(), m_sslParams.dhParams.toStdString());
    return true;
}

bool TorrentImpl::isMoveInProgress() const
{
    return m_storageIsMoving;
}

void TorrentImpl::updateStatus(const lt::torrent_status &nativeStatus)
{
    // Since libtorrent alerts are handled asynchronously there can be obsolete
    // "state update" event reached here after torrent was reloaded in libtorrent.
    // Just discard such events.
    if (nativeStatus.handle != m_nativeHandle) [[unlikely]]
        return;

    const lt::torrent_status oldStatus = std::exchange(m_nativeStatus, nativeStatus);

    if (m_nativeStatus.num_pieces != oldStatus.num_pieces)
        updateProgress();

    if (m_nativeStatus.completed_time != oldStatus.completed_time)
        m_completedTime = (m_nativeStatus.completed_time > 0) ? QDateTime::fromSecsSinceEpoch(m_nativeStatus.completed_time) : QDateTime();

    if (m_nativeStatus.last_seen_complete != oldStatus.last_seen_complete)
        m_lastSeenComplete = QDateTime::fromSecsSinceEpoch(m_nativeStatus.last_seen_complete);

    updateState();

    m_payloadRateMonitor.addSample({nativeStatus.download_payload_rate
                              , nativeStatus.upload_payload_rate});

    if (hasMetadata())
    {
        // NOTE: Don't change the order of these conditionals!
        // Otherwise it will not work properly since torrent can be CheckingDownloading.
        if (isChecking())
            m_unchecked = false;
        else if (isDownloading())
            m_unchecked = true;
    }

    while (!m_statusUpdatedTriggers.isEmpty())
        std::invoke(m_statusUpdatedTriggers.dequeue());
}

void TorrentImpl::updateProgress()
{
    Q_ASSERT(hasMetadata());
    if (!hasMetadata()) [[unlikely]]
        return;

    Q_ASSERT(!m_filesProgress.isEmpty());
    if (m_filesProgress.isEmpty()) [[unlikely]]
        m_filesProgress.resize(filesCount());

    const QBitArray oldPieces = std::exchange(m_pieces, LT::toQBitArray(m_nativeStatus.pieces));
    const QBitArray newPieces = m_pieces ^ oldPieces;

    const int64_t pieceSize = m_torrentInfo.pieceLength();
    for (qsizetype index = 0; index < newPieces.size(); ++index)
    {
        if (!newPieces.at(index))
            continue;

        int64_t size = m_torrentInfo.pieceLength(index);
        int64_t pieceOffset = index * pieceSize;

        for (const int fileIndex : asConst(m_torrentInfo.fileIndicesForPiece(index)))
        {
            const int64_t fileOffsetInPiece = pieceOffset - m_torrentInfo.fileOffset(fileIndex);
            const int64_t add = std::min<int64_t>((m_torrentInfo.fileSize(fileIndex) - fileOffsetInPiece), size);

            m_filesProgress[fileIndex] += add;

            size -= add;
            if (size <= 0)
                break;

            pieceOffset += add;
        }
    }
}

void TorrentImpl::setRatioLimit(qreal limit)
{
    if (limit < DEFAULT_RATIO_LIMIT)
        limit = NO_RATIO_LIMIT;

    if (m_ratioLimit != limit)
    {
        m_ratioLimit = limit;
        deferredRequestResumeData();
        m_session->handleTorrentShareLimitChanged(this);
    }
}

void TorrentImpl::setSeedingTimeLimit(int limit)
{
    if (limit < DEFAULT_SEEDING_TIME_LIMIT)
        limit = NO_SEEDING_TIME_LIMIT;

    if (m_seedingTimeLimit != limit)
    {
        m_seedingTimeLimit = limit;
        deferredRequestResumeData();
        m_session->handleTorrentShareLimitChanged(this);
    }
}

void TorrentImpl::setInactiveSeedingTimeLimit(int limit)
{
    if (limit < DEFAULT_SEEDING_TIME_LIMIT)
        limit = NO_SEEDING_TIME_LIMIT;

    if (m_inactiveSeedingTimeLimit != limit)
    {
        m_inactiveSeedingTimeLimit = limit;
        deferredRequestResumeData();
        m_session->handleTorrentShareLimitChanged(this);
    }
}

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

void TorrentImpl::setShareLimitAction(const ShareLimitAction action)
{
    if (m_shareLimitAction != action)
    {
        m_shareLimitAction = action;
        deferredRequestResumeData();
        m_session->handleTorrentShareLimitChanged(this);
    }
}

void TorrentImpl::setUploadLimit(const int limit)
{
    const int cleanValue = cleanLimitValue(limit);
    if (cleanValue == uploadLimit())
        return;

    m_uploadLimit = cleanValue;
    m_nativeHandle.set_upload_limit(m_uploadLimit);
    deferredRequestResumeData();
}

void TorrentImpl::setDownloadLimit(const int limit)
{
    const int cleanValue = cleanLimitValue(limit);
    if (cleanValue == downloadLimit())
        return;

    m_downloadLimit = cleanValue;
    m_nativeHandle.set_download_limit(m_downloadLimit);
    deferredRequestResumeData();
}

void TorrentImpl::setSuperSeeding(const bool enable)
{
    if (enable == superSeeding())
        return;

    if (enable)
        m_nativeHandle.set_flags(lt::torrent_flags::super_seeding);
    else
        m_nativeHandle.unset_flags(lt::torrent_flags::super_seeding);

    deferredRequestResumeData();
}

void TorrentImpl::setDHTDisabled(const bool disable)
{
    if (disable == isDHTDisabled())
        return;

    if (disable)
        m_nativeHandle.set_flags(lt::torrent_flags::disable_dht);
    else
        m_nativeHandle.unset_flags(lt::torrent_flags::disable_dht);

    deferredRequestResumeData();
}

void TorrentImpl::setPEXDisabled(const bool disable)
{
    if (disable == isPEXDisabled())
        return;

    if (disable)
        m_nativeHandle.set_flags(lt::torrent_flags::disable_pex);
    else
        m_nativeHandle.unset_flags(lt::torrent_flags::disable_pex);

    deferredRequestResumeData();
}

void TorrentImpl::setLSDDisabled(const bool disable)
{
    if (disable == isLSDDisabled())
        return;

    if (disable)
        m_nativeHandle.set_flags(lt::torrent_flags::disable_lsd);
    else
        m_nativeHandle.unset_flags(lt::torrent_flags::disable_lsd);

    deferredRequestResumeData();
}

void TorrentImpl::flushCache() const
{
    m_nativeHandle.flush_cache();
}

QString TorrentImpl::createMagnetURI() const
{
    QString ret = u"magnet:?"_s;

    const SHA1Hash infoHash1 = infoHash().v1();
    if (infoHash1.isValid())
        ret += u"xt=urn:btih:" + infoHash1.toString();

    if (const SHA256Hash infoHash2 = infoHash().v2(); infoHash2.isValid())
    {
        if (infoHash1.isValid())
            ret += u'&';
        ret += u"xt=urn:btmh:1220" + infoHash2.toString();
    }

    if (const QString displayName = name(); displayName != id().toString())
        ret += u"&dn=" + QString::fromLatin1(QUrl::toPercentEncoding(displayName));

    if (hasMetadata())
        ret += u"&xl=" + QString::number(totalSize());

    for (const TrackerEntryStatus &tracker : asConst(trackers()))
        ret += u"&tr=" + QString::fromLatin1(QUrl::toPercentEncoding(tracker.url));

    for (const QUrl &urlSeed : asConst(urlSeeds()))
        ret += u"&ws=" + urlSeed.toString(QUrl::FullyEncoded);

    return ret;
}

nonstd::expected<lt::entry, QString> TorrentImpl::exportTorrent() const
{
    if (!hasMetadata())
        return nonstd::make_unexpected(tr("Missing metadata"));

    try
    {
        [[maybe_unused]] const auto infoGuard = qScopeGuard([this] { m_ltAddTorrentParams.ti.reset(); });
        m_ltAddTorrentParams.ti = info().nativeInfo();
        return lt::write_torrent_file(m_ltAddTorrentParams);
    }
    catch (const lt::system_error &err)
    {
        return nonstd::make_unexpected(QString::fromLocal8Bit(err.what()));
    }
}

nonstd::expected<QByteArray, QString> TorrentImpl::exportToBuffer() const
{
    const nonstd::expected<lt::entry, QString> preparationResult = exportTorrent();
    if (!preparationResult)
        return nonstd::make_unexpected(preparationResult.error());

    // usually torrent size should be smaller than 1 MB,
    // however there are >100 MB v2/hybrid torrent files out in the wild
    QByteArray buffer;
    buffer.reserve(1024 * 1024);
    lt::bencode(std::back_inserter(buffer), preparationResult.value());
    return buffer;
}

nonstd::expected<void, QString> TorrentImpl::exportToFile(const Path &path) const
{
    const nonstd::expected<lt::entry, QString> preparationResult = exportTorrent();
    if (!preparationResult)
        return nonstd::make_unexpected(preparationResult.error());

    const nonstd::expected<void, QString> saveResult = Utils::IO::saveToFile(path, preparationResult.value());
    if (!saveResult)
        return nonstd::make_unexpected(saveResult.error());

    return {};
}

QFuture<QList<PeerInfo>> TorrentImpl::fetchPeerInfo() const
{
    return invokeAsync([nativeHandle = m_nativeHandle, allPieces = pieces()]() -> QList<PeerInfo>
    {
        try
        {
            std::vector<lt::peer_info> nativePeers;
            nativeHandle.get_peer_info(nativePeers);
            QList<PeerInfo> peers;
            peers.reserve(static_cast<decltype(peers)::size_type>(nativePeers.size()));
            for (const lt::peer_info &peer : nativePeers)
                peers.append(PeerInfo(peer, allPieces));
            return peers;
        }
        catch (const std::exception &) {}

        return {};
    });
}

QFuture<QList<QUrl>> TorrentImpl::fetchURLSeeds() const
{
    return invokeAsync([nativeHandle = m_nativeHandle]() -> QList<QUrl>
    {
        try
        {
            const std::set<std::string> currentSeeds = nativeHandle.url_seeds();
            QList<QUrl> urlSeeds;
            urlSeeds.reserve(static_cast<decltype(urlSeeds)::size_type>(currentSeeds.size()));
            for (const std::string &urlSeed : currentSeeds)
                urlSeeds.append(QString::fromStdString(urlSeed));
            return urlSeeds;
        }
        catch (const std::exception &) {}

        return {};
    });
}

QFuture<QList<int>> TorrentImpl::fetchPieceAvailability() const
{
    return invokeAsync([nativeHandle = m_nativeHandle]() -> QList<int>
    {
        try
        {
            std::vector<int> piecesAvailability;
            nativeHandle.piece_availability(piecesAvailability);
            return QList<int>(piecesAvailability.cbegin(), piecesAvailability.cend());
        }
        catch (const std::exception &) {}

        return {};
    });
}

QFuture<QBitArray> TorrentImpl::fetchDownloadingPieces() const
{
    return invokeAsync([nativeHandle = m_nativeHandle, torrentInfo = m_torrentInfo]() -> QBitArray
    {
        try
        {
#ifdef QBT_USES_LIBTORRENT2
            const std::vector<lt::partial_piece_info> queue = nativeHandle.get_download_queue();
#else
            std::vector<lt::partial_piece_info> queue;
            nativeHandle.get_download_queue(queue);
#endif
            QBitArray result;
            result.resize(torrentInfo.piecesCount());
            for (const lt::partial_piece_info &info : queue)
                result.setBit(LT::toUnderlyingType(info.piece_index));
            return result;
        }
        catch (const std::exception &) {}

        return {};
    });
}

QFuture<QList<qreal>> TorrentImpl::fetchAvailableFileFractions() const
{
    return invokeAsync([nativeHandle = m_nativeHandle, torrentInfo = m_torrentInfo]() -> QList<qreal>
    {
        if (!torrentInfo.isValid() || (torrentInfo.filesCount() <= 0))
            return {};

        try
        {
            std::vector<int> piecesAvailability;
            nativeHandle.piece_availability(piecesAvailability);
            const int filesCount = torrentInfo.filesCount();
            // libtorrent returns empty array for seeding only torrents
            if (piecesAvailability.empty())
                return QList<qreal>(filesCount, -1);

            QList<qreal> result;
            result.reserve(filesCount);
            for (int i = 0; i < filesCount; ++i)
            {
                const TorrentInfo::PieceRange filePieces = torrentInfo.filePieces(i);

                int availablePieces = 0;
                for (const int piece : filePieces)
                    availablePieces += (piecesAvailability[piece] > 0) ? 1 : 0;

                const qreal availability = filePieces.isEmpty()
                        ? 1  // the file has no pieces, so it is available by default
                        : static_cast<qreal>(availablePieces) / filePieces.size();
                result.append(availability);
            }
            return result;
        }
        catch (const std::exception &) {}

        return {};
    });
}

void TorrentImpl::prioritizeFiles(const QList<DownloadPriority> &priorities)
{
    if (!hasMetadata())
        return;

    Q_ASSERT(priorities.size() == filesCount());

    // Reset 'm_hasSeedStatus' if needed in order to react again to
    // "torrent finished" event and e.g. show tray notifications
    const QList<DownloadPriority> oldPriorities = filePriorities();
    for (qsizetype i = 0; i < oldPriorities.size(); ++i)
    {
        if ((oldPriorities[i] == DownloadPriority::Ignored)
            && (priorities[i] > DownloadPriority::Ignored)
            && !m_completedFiles.at(i))
        {
            m_hasFinishedStatus = false;
            break;
        }
    }

    const int internalFilesCount = m_torrentInfo.nativeInfo()->files().num_files(); // including .pad files
    auto nativePriorities = std::vector<lt::download_priority_t>(internalFilesCount, LT::toNative(DownloadPriority::Normal));
    const auto nativeIndexes = m_torrentInfo.nativeIndexes();
    for (qsizetype i = 0; i < priorities.size(); ++i)
        nativePriorities[LT::toUnderlyingType(nativeIndexes[i])] = LT::toNative(priorities[i]);

    qDebug() << Q_FUNC_INFO << "Changing files priorities...";
    m_nativeHandle.prioritize_files(nativePriorities);

    m_filePriorities = priorities;
    // Restore first/last piece first option if necessary
    if (m_hasFirstLastPiecePriority)
        applyFirstLastPiecePriority(true);
    manageActualFilePaths();
}

template <typename Func>
QFuture<std::invoke_result_t<Func>> TorrentImpl::invokeAsync(Func &&func) const
{
    QPromise<std::invoke_result_t<Func>> promise;
    const auto future = promise.future();
    promise.start();
    m_session->invokeAsync([func = std::forward<Func>(func), promise = std::move(promise)]() mutable
    {
        promise.addResult(func());
        promise.finish();
    });

    return future;
}
