// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd.
//
// SPDX-License-Identifier: GPL-3.0-or-later

#include "vfsmonitorwatcher_p.h"

#include <QFileInfo>
#include <QDir>
#include <QFile>
#include <QTextStream>

#include <netlink/genl/ctrl.h>
#include <netlink/genl/genl.h>
#include <netlink/netlink.h>
#include <netlink/socket.h>
#include <netlink/attr.h>
#include <netlink/msg.h>

#include <libmount.h>
#include <sys/sysmacros.h>

#include <cstring>
#include <algorithm>

SERVICETEXTINDEX_BEGIN_NAMESPACE

// Netlink family and multicast group names (matching vfs_genl.h)
static constexpr char kVfsMonitorFamilyName[] = "vfsmonitor";
static constexpr char kVfsMonitorMcgDentry[] = "vfsmonitor_de";

// --- Per-instance C callback for libnl ---

namespace {

struct MountEntry
{
    dev_t deviceId { 0 };
    int parentMountId { 0 };
    QString mountPoint;
    bool isBindMount { false };
    bool isLowerFs { false };
};

// Netlink attribute policy for parsing vfs_monitor messages.
struct nla_policy vfsPolicy[VFSMONITOR_A_MAX + 1];

void initVfsPolicy()
{
    std::memset(vfsPolicy, 0, sizeof(vfsPolicy));
    vfsPolicy[VFSMONITOR_A_ACT].type = NLA_U8;
    vfsPolicy[VFSMONITOR_A_COOKIE].type = NLA_U32;
    vfsPolicy[VFSMONITOR_A_MAJOR].type = NLA_U16;
    vfsPolicy[VFSMONITOR_A_MINOR].type = NLA_U8;
    vfsPolicy[VFSMONITOR_A_PATH].type = NLA_NUL_STRING;
    vfsPolicy[VFSMONITOR_A_PATH].maxlen = 4096;
}

bool mountPointStartsWith(const QString &path, const QString &mountPoint)
{
    if (!path.startsWith(mountPoint))
        return false;

    return mountPoint.endsWith('/') || path.length() == mountPoint.length()
            || path.at(mountPoint.length()) == '/';
}

bool isDescendantOfRoot(const QString &path, const QString &root)
{
    if (root == "/")
        return path.startsWith('/');

    if (!path.startsWith(root))
        return false;

    if (path.length() == root.length())
        return false;

    return root.endsWith('/') || path.at(root.length()) == '/';
}

bool cStringEquals(const char *left, const char *right)
{
    return left && right && qstrcmp(left, right) == 0;
}

bool isLowerFsType(const char *fsType)
{
    return cStringEquals(fsType, "overlay") || cStringEquals(fsType, "fuse.dlnfs")
            || cStringEquals(fsType, "ulnfs");
}

bool isParentChainUnderRoot(const QHash<int, MountEntry> &byMountId, const MountEntry &entry)
{
    if (!entry.isBindMount && entry.mountPoint == "/")
        return true;

    int parentMountId = entry.parentMountId;
    while (parentMountId > 0) {
        auto parentIt = byMountId.find(parentMountId);
        if (parentIt == byMountId.end())
            return false;

        if (parentIt->mountPoint == "/")
            return true;

        parentMountId = parentIt->parentMountId;
    }

    return false;
}

QHash<int, MountEntry> collectMountEntries(libmnt_table *mtab)
{
    QHash<int, MountEntry> byMountId;
    struct libmnt_iter *iter = mnt_new_iter(MNT_ITER_FORWARD);
    if (!iter)
        return byMountId;

    struct libmnt_fs *fs;
    while (mnt_table_next_fs(mtab, iter, &fs) == 0) {
        const char *target = mnt_fs_get_target(fs);
        if (!target)
            continue;

        const char *root = mnt_fs_get_root(fs);
        const char *fsType = mnt_fs_get_fstype(fs);

        MountEntry entry;
        entry.deviceId = mnt_fs_get_devno(fs);
        entry.parentMountId = mnt_fs_get_parent_id(fs);
        entry.mountPoint = QString::fromUtf8(target);
        entry.isBindMount = !cStringEquals(root, "/");
        entry.isLowerFs = isLowerFsType(fsType);

        byMountId.insert(mnt_fs_get_id(fs), entry);
    }

    mnt_free_iter(iter);
    return byMountId;
}

// Per-instance callback: arg is the VfsMonitorFileSystemWatcherPrivate pointer,
// registered per-socket via nl_socket_modify_cb. No global state needed.
int vfsMonitorMsgCallback(struct nl_msg *msg, void *arg)
{
    auto *d = static_cast<VfsMonitorFileSystemWatcherPrivate *>(arg);

    if (!d) {
        return NL_SKIP;
    }

    struct nlattr *tb[VFSMONITOR_A_MAX + 1];
    std::memset(tb, 0, sizeof(tb));

    struct nlmsghdr *nh = nlmsg_hdr(msg);
    int err = genlmsg_parse(nh, 0, tb, VFSMONITOR_A_MAX, vfsPolicy);
    if (err < 0) {
        return NL_SKIP;
    }

    if (!tb[VFSMONITOR_A_PATH]) {
        return NL_SKIP;
    }

    // Extract attributes.
    uint8_t act = tb[VFSMONITOR_A_ACT] ? nla_get_u8(tb[VFSMONITOR_A_ACT]) : 0;
    uint32_t cookie = tb[VFSMONITOR_A_COOKIE] ? nla_get_u32(tb[VFSMONITOR_A_COOKIE]) : 0;
    uint16_t major = tb[VFSMONITOR_A_MAJOR] ? nla_get_u16(tb[VFSMONITOR_A_MAJOR]) : 0;
    uint8_t minor = tb[VFSMONITOR_A_MINOR] ? nla_get_u8(tb[VFSMONITOR_A_MINOR]) : 0;
    const char *pathCStr = reinterpret_cast<const char *>(nla_data(tb[VFSMONITOR_A_PATH]));

    // Skip non-filesystem events.
    if (act > ACT_UNMOUNT) {
        return NL_OK;
    }

    // Mount/unmount events don't carry meaningful path data for indexing,
    // but they invalidate our mount point cache. Flag the dirty bit so
    // handleNetlinkMessage() can refresh the cache after this batch.
    if (act == ACT_MOUNT || act == ACT_UNMOUNT) {
        d->mountPointsDirty = true;
        return NL_OK;
    }

    dev_t deviceId = makedev(major, minor);

    // RENAME_FROM events must be stored before filtering, because the paired
    // RENAME_TO may land in an excluded path (e.g., trash). We need the
    // RENAME_FROM info to emit a delete when that happens.
    if (act == ACT_RENAME_FROM_FILE || act == ACT_RENAME_FROM_FOLDER) {
        QString fullPath = d->resolveAndFilterFullPath(deviceId, pathCStr);
        if (fullPath.isNull())
            return NL_OK;   // Source itself is excluded, no point tracking.

        auto [parentPath, name] = VfsMonitorFileSystemWatcherPrivate::splitPath(fullPath);

        RenameFromInfo info;
        info.path = parentPath;
        info.name = name;
        info.isDirectory = (act == ACT_RENAME_FROM_FOLDER);
        d->pendingRenames.insert(cookie, info);
        return NL_OK;
    }

    // RENAME_TO events need independent path resolution because the target may
    // be filtered (e.g., moved to trash). In that case we emit a delete for the
    // source instead of a move — the generic fullPath.isNull() gate below would
    // swallow this event before we could check pendingRenames.
    if (act == ACT_RENAME_TO_FILE || act == ACT_RENAME_TO_FOLDER) {
        auto *q = d->q_ptr;
        const bool isDir = (act == ACT_RENAME_TO_FOLDER);

        auto it = d->pendingRenames.find(cookie);
        if (it != d->pendingRenames.end()) {
            QString fullPath = d->resolveAndFilterFullPath(deviceId, pathCStr);
            if (!fullPath.isNull()) {
                auto [parentPath, name] = VfsMonitorFileSystemWatcherPrivate::splitPath(fullPath);
                if (isDir)
                    Q_EMIT q->directoryMoved(it->path, it->name, parentPath, name);
                else
                    Q_EMIT q->fileMoved(it->path, it->name, parentPath, name);
            } else {
                // Target is filtered (e.g., moved to trash) → source is gone.
                if (isDir)
                    Q_EMIT q->directoryDeleted(it->path, it->name);
                else
                    Q_EMIT q->fileDeleted(it->path, it->name);
            }
            d->pendingRenames.erase(it);
        } else {
            // Unpaired RENAME_TO: treat as creation if in monitored area.
            QString fullPath = d->resolveAndFilterFullPath(deviceId, pathCStr);
            if (!fullPath.isNull()) {
                auto [parentPath, name] = VfsMonitorFileSystemWatcherPrivate::splitPath(fullPath);
                if (isDir)
                    Q_EMIT q->directoryCreated(parentPath, name);
                else
                    Q_EMIT q->fileCreated(parentPath, name);
            }
        }
        return NL_OK;
    }

    // For all other events, reconstruct and filter the full path.
    QString fullPath = d->resolveAndFilterFullPath(deviceId, pathCStr);
    if (fullPath.isNull()) {
        return NL_OK;
    }

    auto *q = d->q_ptr;
    auto [parentPath, name] = VfsMonitorFileSystemWatcherPrivate::splitPath(fullPath);

    switch (act) {
    case ACT_NEW_FILE:
    case ACT_NEW_LINK:
    case ACT_NEW_SYMLINK:
        Q_EMIT q->fileCreated(parentPath, name);
        break;

    case ACT_NEW_FOLDER:
        Q_EMIT q->directoryCreated(parentPath, name);
        break;

    case ACT_DEL_FILE:
        Q_EMIT q->fileDeleted(parentPath, name);
        break;

    case ACT_DEL_FOLDER:
        Q_EMIT q->directoryDeleted(parentPath, name);
        break;

        // Synthetic events (kernel-paired rename).
    case ACT_RENAME_FILE:
        Q_EMIT q->fileCreated(parentPath, name);
        break;

    case ACT_RENAME_FOLDER:
        Q_EMIT q->directoryCreated(parentPath, name);
        break;

    default:
        break;
    }

    return NL_OK;
}

static bool registerMessageCallback(nl_sock *sock, VfsMonitorFileSystemWatcherPrivate *d)
{
    // Pass d directly as per-socket callback data. Each nl_sock gets its own
    // callback arg, so multiple instances are naturally supported.
    int ret = nl_socket_modify_cb(sock, NL_CB_VALID, NL_CB_CUSTOM,
                                  vfsMonitorMsgCallback, d);
    if (ret != 0) {
        return false;
    }

    return true;
}

}   // anonymous namespace

// ========== VfsMonitorFileSystemWatcherPrivate ==========

VfsMonitorFileSystemWatcherPrivate::VfsMonitorFileSystemWatcherPrivate(
        const QStringList &rootPaths,
        VfsMonitorFileSystemWatcher::PathExcludePredicate excludePredicate,
        VfsMonitorFileSystemWatcher *qq)
    : q_ptr(qq), rootPaths(rootPaths), excludePredicate(std::move(excludePredicate))
{
}

VfsMonitorFileSystemWatcherPrivate::~VfsMonitorFileSystemWatcherPrivate()
{
    if (notifier) {
        notifier->setEnabled(false);
    }

    // No global callback state to clear — the per-socket callback data (this
    // pointer) becomes invalid when nlSock is freed below, and the callback
    // checks for null before dereferencing.

    if (nlSock) {
        nl_socket_free(nlSock);
        nlSock = nullptr;
    }
}

bool VfsMonitorFileSystemWatcherPrivate::initMountPoints()
{
    mountPoints.clear();
    childMountPoints.clear();
    lowerFsExists = false;

    struct libmnt_table *mtab = mnt_new_table();
    if (!mtab) {
        return false;
    }

    if (mnt_table_parse_mtab(mtab, nullptr) < 0) {
        mnt_free_table(mtab);
        return false;
    }

    const QHash<int, MountEntry> byMountId = collectMountEntries(mtab);
    mnt_free_table(mtab);

    QHash<int, MountEntry> rootMountTree;
    for (auto it = byMountId.cbegin(); it != byMountId.cend(); ++it) {
        const auto &entry = it.value();

        if (!isParentChainUnderRoot(byMountId, entry))
            continue;

        mountPoints[entry.deviceId].append(entry.mountPoint);
        rootMountTree.insert(it.key(), entry);
        lowerFsExists = lowerFsExists || entry.isLowerFs;
    }

    for (auto &points : mountPoints) {
        std::sort(points.begin(), points.end(),
                  [](const QString &a, const QString &b) {
                      return a.length() > b.length();
                  });
    }

    for (auto it = rootMountTree.cbegin(); it != rootMountTree.cend(); ++it) {
        const auto &parent = it.value();
        QStringList children;
        for (const auto &entry : std::as_const(rootMountTree)) {
            if (entry.parentMountId == it.key()) {
                children.append(entry.mountPoint);
            }
        }

        if (!children.isEmpty())
            childMountPoints[parent.deviceId].append(children);
    }

    for (auto &points : childMountPoints) {
        points.removeDuplicates();
        std::sort(points.begin(), points.end(),
                  [](const QString &a, const QString &b) {
                      return a.length() > b.length();
                  });
    }

    int totalPoints = 0;
    for (const auto &pts : std::as_const(mountPoints)) {
        totalPoints += pts.size();
    }
    fmInfo() << "VfsMonitor: loaded" << mountPoints.size()
             << "devices," << totalPoints << "mount points,"
             << childMountPoints.size() << "devices with child mount points,"
             << "lowerfs exists:" << lowerFsExists;
    return !mountPoints.isEmpty();
}

bool VfsMonitorFileSystemWatcherPrivate::isLowerFsEvent(dev_t deviceId, const QString &fullPath) const
{
    if (!lowerFsExists)
        return false;

    auto it = childMountPoints.find(deviceId);
    if (it == childMountPoints.end())
        return false;

    for (const QString &childMountPoint : it.value()) {
        if (mountPointStartsWith(fullPath, childMountPoint)) {
            return true;
        }
    }

    return false;
}

QString VfsMonitorFileSystemWatcherPrivate::resolveAndFilterFullPath(dev_t deviceId,
                                                                     const char *relativePath) const
{
    auto it = mountPoints.find(deviceId);
    if (it == mountPoints.end())
        return {};

    const QStringList &points = it.value();
    const QString relPath = QString::fromUtf8(relativePath);

    // Try each mount point (sorted longest first).
    // Return the first one that falls under a monitored root path
    // and passes the exclude predicate.
    for (const QString &mp : points) {
        QString fullPath = (mp == "/") ? relPath : (mp + relPath);

        if (isLowerFsEvent(deviceId, fullPath))
            continue;

        if (std::none_of(rootPaths.cbegin(), rootPaths.cend(),
                         [&fullPath](const QString &root) { return isDescendantOfRoot(fullPath, root); }))
            continue;

        if (excludePredicate && excludePredicate(fullPath))
            continue;

        return fullPath;
    }

    // In overlay root environments, the root "/" filesystem has no separate mount
    // entry in mountPoints. When all mount points fail and relPath is already
    // an absolute path, treat it as the true absolute path and re-check.
    if (relPath.startsWith('/')) {
        auto found = std::find_if(rootPaths.cbegin(), rootPaths.cend(),
                                  [&relPath, this](const QString &root) {
                                      return isDescendantOfRoot(relPath, root)
                                              && (!excludePredicate || !excludePredicate(relPath));
                                  });
        if (found != rootPaths.cend())
            return relPath;
    }

    return {};
}

QPair<QString, QString> VfsMonitorFileSystemWatcherPrivate::splitPath(const QString &fullPath)
{
    QFileInfo fi(fullPath);
    return qMakePair(fi.absolutePath(), fi.fileName());
}

bool VfsMonitorFileSystemWatcherPrivate::initNetlink()
{
    Q_Q(VfsMonitorFileSystemWatcher);

    // Build mount point cache first (no netlink dependency).
    if (!initMountPoints()) {
        fmWarning() << "VfsMonitor: failed to initialize mount points";
    }

    nlSock = nl_socket_alloc();
    if (!nlSock) {
        fmWarning() << "VfsMonitor: failed to allocate netlink socket";
        return false;
    }

    if (genl_connect(nlSock) != 0) {
        fmWarning() << "VfsMonitor: genl_connect failed";
        nl_socket_free(nlSock);
        nlSock = nullptr;
        return false;
    }

    // Set receive buffer to system maximum to prevent event loss.
    QFile rmemFile("/proc/sys/net/core/rmem_max");
    if (rmemFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
        QTextStream in(&rmemFile);
        bool ok = false;
        int maxRcvbuf = in.readLine().trimmed().toInt(&ok);
        if (ok && maxRcvbuf > 0) {
            if (nl_socket_set_buffer_size(nlSock, maxRcvbuf, 0) != 0) {
                fmWarning() << "VfsMonitor: failed to set socket buffer size";
            }
        }
        rmemFile.close();
    }

    nl_socket_disable_seq_check(nlSock);
    nl_socket_disable_auto_ack(nlSock);

    int mcgrpId = genl_ctrl_resolve_grp(nlSock, kVfsMonitorFamilyName, kVfsMonitorMcgDentry);
    if (mcgrpId < 0) {
        fmWarning() << "VfsMonitor: vfs_monitor kernel module not available";
        nl_socket_free(nlSock);
        nlSock = nullptr;
        return false;
    }

    if (nl_socket_add_membership(nlSock, mcgrpId) != 0) {
        fmWarning() << "VfsMonitor: failed to join multicast group";
        nl_socket_free(nlSock);
        nlSock = nullptr;
        return false;
    }

    initVfsPolicy();

    if (!registerMessageCallback(nlSock, this)) {
        nl_socket_free(nlSock);
        nlSock = nullptr;
        return false;
    }

    int fd = nl_socket_get_fd(nlSock);
    notifier = new QSocketNotifier(fd, QSocketNotifier::Read, q);
    QObject::connect(notifier, &QSocketNotifier::activated, q, [this]() {
        handleNetlinkMessage();
    });

    fmInfo() << "VfsMonitor: connected to vfs_monitor kernel module";
    return true;
}

void VfsMonitorFileSystemWatcherPrivate::handleNetlinkMessage()
{
    int ret = nl_recvmsgs_default(nlSock);
    if (ret < 0 && ret != -NLE_AGAIN) {
        fmWarning() << "VfsMonitor: nl_recvmsgs_default failed, error:" << ret;
    }

    // Refresh mount point cache if mount/unmount events were received.
    // Done after nl_recvmsgs_default() so the cache is up-to-date for the
    // next batch of file events.
    if (mountPointsDirty) {
        mountPointsDirty = false;
        if (initMountPoints()) {
            fmInfo() << "VfsMonitor: mount point cache refreshed";
        } else {
            fmWarning() << "VfsMonitor: failed to refresh mount point cache";
        }
    }

    // Clean up orphaned RENAME_FROM entries.
    static constexpr int kPendingRenameCleanupThreshold = 1000;
    if (pendingRenames.size() > kPendingRenameCleanupThreshold) {
        fmWarning() << "VfsMonitor: pending rename table too large ("
                    << pendingRenames.size() << "), clearing";
        pendingRenames.clear();
    }
}

// ========== VfsMonitorFileSystemWatcher ==========

VfsMonitorFileSystemWatcher::VfsMonitorFileSystemWatcher(const QStringList &rootPaths,
                                                         PathExcludePredicate excludePredicate,
                                                         QObject *parent)
    : QObject(parent), d_ptr(new VfsMonitorFileSystemWatcherPrivate(rootPaths, std::move(excludePredicate), this))
{
}

VfsMonitorFileSystemWatcher::~VfsMonitorFileSystemWatcher()
{
}

VfsMonitorFileSystemWatcher *VfsMonitorFileSystemWatcher::create(const QStringList &rootPaths,
                                                                 PathExcludePredicate excludePredicate,
                                                                 QObject *parent)
{
    auto *watcher = new VfsMonitorFileSystemWatcher(rootPaths, std::move(excludePredicate), parent);

    if (!watcher->d_func()->initNetlink()) {
        delete watcher;
        return nullptr;
    }

    return watcher;
}

SERVICETEXTINDEX_END_NAMESPACE
