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

#include "core/default_event_handler.h"

#include <cstdlib> // std::getenv
#include <unordered_set>
#include <glib.h>
#include <gmodule.h>
#include <sys/sysmacros.h>

#include "utils/log.h"
#include "utils/string_helper.h"
#include "vfs_change_consts.h"
#include "core/config.h"
#include "utils/tools.h"

ANYTHING_NAMESPACE_BEGIN

#define ACT_TERMINATE 100

#define ACT_BLACKLIST_ADD_ABSOLUTE_PATH 120
#define ACT_BLACKLIST_DEL_ABSOLUTE_PATH 121
#define ACT_BLACKLIST_ADD_OTHER         122
#define ACT_CONFIG_EVENT_END            130


static std::string determine_blacklist_path(MountInfo *mount_info, const char *path)
{
    std::string ret;

    // process_blacklist_paths ensure no empty path
    if (path[0] != '/') {
        ret = path;
    } else {
        if (g_file_test(path, G_FILE_TEST_EXISTS)) {
            g_autofree char *event_path = get_full_path(mount_info, path);
            if (event_path) {
                ret = event_path;
            }
        }
    }

    spdlog::info("Determine blacklist path: \"{}\" -> \"{}\"", path, ret);

    return ret;
}

// 检查 event_path 与 indexing_items_ 中的 event_path 是否冲突
bool is_event_path_conflict_with_indexing_items(const std::string& event_path,
                                                const std::vector<indexing_item>& indexing_items) {
    for (auto& item : indexing_items) {
        if (string_helper::starts_with(event_path, item.event_path) ||
            string_helper::starts_with(item.event_path, event_path)) {
            return true;
        }
    }
    return false;
}

std::string get_event_path(MountInfo *mount_info, const std::string& origin_path, const std::vector<indexing_item>& indexing_items) {
    std::error_code ec;
    if (!std::filesystem::exists(origin_path, ec)) {
        spdlog::error("The origin path {} does not exist: {}", origin_path, ec.message());
        return "";
    }

    std::string event_path_str;
    char *event_path = get_full_path(mount_info, origin_path.c_str());
    if (event_path == nullptr) {
        spdlog::warn("Failed to get event path, use the origin path: {}", origin_path);
        event_path_str = origin_path;
    } else {
        event_path_str = std::string(event_path);
        g_free(event_path);
    }

    // add "/" to the end of the event_path for simplify the comparison
    std::string event_path_with_slash = event_path_str;
    if (!string_helper::ends_with(event_path_with_slash, "/")) {
        event_path_with_slash += "/";
    }

    if (is_event_path_conflict_with_indexing_items(event_path_with_slash, indexing_items)) {
        spdlog::warn("Event path {} conflicts with already configured event paths, skip", event_path_with_slash);
        return "";
    }

    return event_path_with_slash;
}

// /data 和非 data 需要保持一致，最好有一种方式能够获取当前的状态
default_event_handler::default_event_handler(const event_handler_config &config)
    : base_event_handler(config), config_(config) {
    event_queue_ = g_async_queue_new();
    event_filter_thread_ = g_thread_new("event_filter", event_filter_thread_func, this);
    config_event_queue_ = g_async_queue_new();
    mount_info_ = mount_info_new();

    // init indexing_items_
    spdlog::info("Processing indexing_paths...");
    for (auto& origin_path : config_.indexing_paths) {
        std::string event_path_with_slash = get_event_path(mount_info_, origin_path, indexing_items_);
        if (event_path_with_slash.empty()) {
            continue;
        }

        std::string origin_path_with_slash = origin_path;
        if (!string_helper::ends_with(origin_path_with_slash, "/")) {
            origin_path_with_slash += "/";
        }
        indexing_item item = {
            .origin_path = origin_path_with_slash,
            .event_path = event_path_with_slash,
            .different_path = origin_path_with_slash != event_path_with_slash,
            .enable = false
        };
        spdlog::info("Determine the event path: {} -> {}", item.origin_path, item.event_path);

        indexing_items_.emplace_back(item);
    }

    // init event_path_blocked_list_
    spdlog::info("Processing blacklist_paths...");
    for (auto& path : config_.blacklist_paths) {
        std::string event_path = determine_blacklist_path(mount_info_, path.c_str());
        if (event_path.empty())
            continue;
        event_path_blocked_list_.emplace_back(event_path);
    }

    // index refresh and scan
    std::vector<std::string> index_dirs;
    for (const auto& item : indexing_items_) {
        std::string scan_path_without_slash = item.origin_path;
        if (scan_path_without_slash != "/") {
            scan_path_without_slash.pop_back();
        }
        index_dirs.push_back(std::move(scan_path_without_slash));
    }
    init_refresh_scan_indexes(index_dirs);

    g_autofree gchar *dump = mount_info_dump(mount_info_);
    // remove the last "\n"
    dump[strlen(dump) - 1] = '\0';
    spdlog::info("{}", dump);
}

default_event_handler::~default_event_handler() {
    mount_info_free(mount_info_);
}

bool default_event_handler::is_under_indexing_path(const std::string& path, indexing_item *&indexing_item) {
    for (auto& item : indexing_items_) {
        if (item.enable && string_helper::starts_with(path, item.event_path)) {
            indexing_item = &item;
            return true;
        }
    }
    return false;
}

// 将路径中的 /persistent/home/user 替换为 /home/user
void default_event_handler::convert_event_path_to_origin_path(std::string& path, const indexing_item& item) {
    if (item.different_path) {
        path.replace(0, item.event_path.length(), item.origin_path);
    }
}

bool default_event_handler::is_event_path_blocked(const std::string& path, indexing_item *&indexing_item) {
    return !is_under_indexing_path(path, indexing_item) || is_path_in_blacklist(path, event_path_blocked_list_);
}

void default_event_handler::handle(fs_event *event) {
    g_async_queue_push(event_queue_, event);
}

void default_event_handler::start_handle_init_scan(const std::string &path) {
    for (auto& item : indexing_items_) {
        if (item.enable)
            continue;

        std::string origin_path_without_slash = item.origin_path;
        if (origin_path_without_slash != "/")
            origin_path_without_slash.pop_back();

        if (origin_path_without_slash == path) {
            item.enable = true;
            break;
        }
    }
}

// 将 fs_event 转换为 fs_event_with_full_path
// return true 表示处理完成，false 表示需要继续处理
bool default_event_handler::convert_fs_event(fs_event *event, fs_event_with_full_path *event_with_full_path) {
    // Update partition event
    if (event->act == ACT_MOUNT || event->act == ACT_UNMOUNT) {
        spdlog::debug("{}: {}", (event->act == ACT_MOUNT ? "Mount a device" : "Unmount a device"), event->src);
        mount_info_update(mount_info_);
        return true;
    }

    std::string root;
    if (event->act < ACT_MOUNT) {
        event_with_full_path->device_id = makedev(event->major, event->minor);

        const char *mount_point = mount_info_get_device_mount_point(mount_info_, event_with_full_path->device_id);
        if (!mount_point) {
            spdlog::debug("Unknown device: {}, dev: {}:{}, path: {}, cookie: {}",
                +event->act, event->major, +event->minor, event->src, event->cookie);
            return true;
        }
        root = mount_point;
        if (root == "/")
            root.clear();
    }

    switch (event->act) {
    case ACT_NEW_FILE:
    case ACT_NEW_SYMLINK:
    case ACT_NEW_LINK:
    case ACT_NEW_FOLDER:
    case ACT_DEL_FILE:
    case ACT_DEL_FOLDER:
        {
            event_with_full_path->act = event->act;
            event_with_full_path->src = event->src;
            event_with_full_path->dst = "";
        }
        break;
    case ACT_RENAME_FROM_FILE:
    case ACT_RENAME_FROM_FOLDER:
        rename_from_.emplace(event->cookie, event->src);
        return true;
    case ACT_RENAME_TO_FILE:
    case ACT_RENAME_TO_FOLDER:
        if (auto search = rename_from_.find(event->cookie);
            search != rename_from_.end()) {
            event_with_full_path->act = event->act == ACT_RENAME_TO_FILE ? ACT_RENAME_FILE : ACT_RENAME_FOLDER;
            event_with_full_path->dst = event->src;
            event_with_full_path->src = rename_from_[event->cookie];
        }
        break;
    case ACT_RENAME_FILE:
    case ACT_RENAME_FOLDER:
        spdlog::warn("Don't support file action: {}", +event->act);
        return true;
    default:
        spdlog::warn("Unknown file action: {}", +event->act);
        return true;
    }

    if (!root.empty()) {
        event_with_full_path->src = root + event_with_full_path->src;
        if (!event_with_full_path->dst.empty())
            event_with_full_path->dst = root + event_with_full_path->dst;
    }

    if (event->act == ACT_RENAME_FILE || event->act == ACT_RENAME_FOLDER) {
        rename_from_.erase(event->cookie);
    }

    return false;
}

bool is_lowerfs_event(MountInfo *mount_info, fs_event_with_full_path *event) {
    if (!mount_info_exist_lowerfs(mount_info)) {
        return false;
    }

    const char* event_file_path = event->dst.empty() ? event->src.c_str() : event->dst.c_str();

    const GList *child_mount_points = mount_info_get_child_mount_points(mount_info, event->device_id);
    if (child_mount_points) {
        for (const GList *iter = child_mount_points; iter != NULL; iter = iter->next) {
            if (string_helper::starts_with(event_file_path, (const gchar *)iter->data)) {
                spdlog::debug("{}:{} {} is under the child mount point: {}",
                    major(event->device_id), minor(event->device_id), event_file_path, (gchar *)iter->data);
                return true;
            }
        }
    }

    return false;
}

void default_event_handler::filter_event(fs_event *fs_event) {
    [[maybe_unused]] const char* act_names[] = {"file_created", "link_created", "symlink_created", "dir_created", "file_deleted", "dir_deleted", "file_renamed", "dir_renamed"};

    fs_event_with_full_path event;
    if (convert_fs_event(fs_event, &event)) {
        return;
    }

    spdlog::debug("Received event: {} {} {}", act_names[event.act], event.src, event.dst);

    // Preparations are done, starting to process the event.

    indexing_item *src_indexing_item = nullptr;
    indexing_item *dst_indexing_item = nullptr;
    if (event.act != ACT_RENAME_FILE &&
        event.act != ACT_RENAME_FOLDER &&
        is_event_path_blocked(event.src, src_indexing_item)) {
        return;
    }

    if (is_lowerfs_event(mount_info_, &event)) {
        return;
    }

    if (event.act == ACT_NEW_FILE || event.act == ACT_NEW_SYMLINK ||
        event.act == ACT_NEW_LINK || event.act == ACT_NEW_FOLDER) {
        // Do not check for the existence of files; we trust the kernel module.
        convert_event_path_to_origin_path(event.src, *src_indexing_item);
        add_index_delay(std::move(event.src));
    } else if (event.act == ACT_DEL_FILE || event.act == ACT_DEL_FOLDER) {
        convert_event_path_to_origin_path(event.src, *src_indexing_item);
        remove_index_delay(std::move(event.src));
    } else if (event.act == ACT_RENAME_FILE) {
        bool isSrcBlocked = is_event_path_blocked(event.src, src_indexing_item);
        bool isDstBlocked = is_event_path_blocked(event.dst, dst_indexing_item);

        if (isSrcBlocked && isDstBlocked) {
            return;
        } else if (isSrcBlocked) {
            convert_event_path_to_origin_path(event.dst, *dst_indexing_item);
            add_index_delay(std::move(event.dst));
        } else if (isDstBlocked) {
            convert_event_path_to_origin_path(event.src, *src_indexing_item);
            remove_index_delay(std::move(event.src));
        } else {
            convert_event_path_to_origin_path(event.src, *src_indexing_item);
            convert_event_path_to_origin_path(event.dst, *dst_indexing_item);
            update_index_delay(std::move(event.src), std::move(event.dst));
        }
    } else if (event.act == ACT_RENAME_FOLDER) {
        // Rename all files/folders in this folder(including this folder)
        bool isSrcBlocked = is_event_path_blocked(event.src, src_indexing_item);
        bool isDstBlocked = is_event_path_blocked(event.dst, dst_indexing_item);

        if (isSrcBlocked && isDstBlocked) {
            return;
        }

        if (isSrcBlocked) {
            convert_event_path_to_origin_path(event.dst, *dst_indexing_item);
            add_index_delay(event.dst);
            scan_index_delay(std::move(event.dst));
            return;
        }

        convert_event_path_to_origin_path(event.src, *src_indexing_item);
        if (!isDstBlocked) {
            convert_event_path_to_origin_path(event.dst, *dst_indexing_item);
        } else {
            event.dst.clear();
        }
        recursive_update_index_delay(std::move(event.src), std::move(event.dst));
    }
}

void default_event_handler::terminate_filter() {
    fs_event* event = g_slice_new(fs_event);
    event->act = ACT_TERMINATE;
    g_async_queue_push(event_queue_, event);
    g_thread_join(event_filter_thread_);
    spdlog::info("Event filter thread terminated");
    g_async_queue_unref(event_queue_);
    g_async_queue_unref(config_event_queue_);
}

void* default_event_handler::event_filter_thread_func(void* data) {
    auto handler = static_cast<default_event_handler*>(data);
    while (true) {
        // If no file event is processed within 3 seconds, check the config event.
        fs_event* event = (fs_event*)g_async_queue_timeout_pop(handler->event_queue_, 3 * 1000 * 1000);
        if (!event) {
            handler->handle_config_event();
            continue;
        }
        if (event->act == ACT_TERMINATE) {
            g_slice_free(fs_event, event);
            break;
        }
        handler->filter_event(event);
        g_slice_free(fs_event, event);
    }

    return NULL;
}

static void get_string_list_diff(const std::vector<std::string> &olds,
                                 const std::vector<std::string> &news,
                                 std::vector<std::string> *adds,
                                 std::vector<std::string> *dels)
{
    if (adds) {
        adds->clear();
        std::unordered_set<std::string> old_set(olds.begin(), olds.end());
        for (const auto& s : news) {
            if (old_set.find(s) == old_set.end()) {
                adds->push_back(s);
            }
        }
    }

    if (dels) {
        dels->clear();
        std::unordered_set<std::string> new_set(news.begin(), news.end());
        for (const auto& s : olds) {
            if (new_set.find(s) == new_set.end()) {
                dels->push_back(s);
            }
        }
    }
}

static bool is_all_absolute_path(const std::vector<std::string> &paths)
{
    for (const auto& path : paths) {
        // process_blacklist_paths ensure no empty path
        if (path[0] != '/') {
            return false;
        }
    }
    return true;
}

static void push_config_event(GAsyncQueue *queue, uint8_t act, const char* path)
{
    fs_event* event = g_slice_new(fs_event);

    event->act = act;
    strncpy(event->src, path, MAX_PATH_LEN-1);
    if (event->src[MAX_PATH_LEN-2] != '\0') {
        spdlog::warn("File path is too long: {}", path);
        g_slice_free(fs_event, event);
        return;
    }

    g_async_queue_push(queue, event);
}

bool default_event_handler::handle_blacklist_paths_change(const event_handler_config &old_config, const event_handler_config &new_config)
{
    std::vector<std::string> adds, dels;
    get_string_list_diff(old_config.blacklist_paths,
                         new_config.blacklist_paths,
                         &adds,
                         &dels);

    if (!dels.empty() && !is_all_absolute_path(dels)) {
        spdlog::info("Found non-absolute paths were removed from the blacklist");
        return false;
    }

    if (!adds.empty()) {
        uint8_t act = is_all_absolute_path(adds) ? ACT_BLACKLIST_ADD_ABSOLUTE_PATH : ACT_BLACKLIST_ADD_OTHER;
        for (const auto& path : adds) {
            push_config_event(config_event_queue_, act, path.c_str());
        }
    }

    if (!dels.empty()) {
        for (const auto& path : dels) {
            push_config_event(config_event_queue_, ACT_BLACKLIST_DEL_ABSOLUTE_PATH, path.c_str());
        }
    }

    // indicate the end of the events
    if (!adds.empty() || !dels.empty()) {
        push_config_event(config_event_queue_, ACT_CONFIG_EVENT_END, "");
    } else {
        spdlog::info("No valid config updates found");
    }

    return true;
}

bool default_event_handler::handle_config_change(const std::string &key, const event_handler_config &new_config)
{
    if (!base_event_handler::handle_config_change(key, new_config))
        return false;

    bool handled = (key == "blacklist_paths" && handle_blacklist_paths_change(config_, new_config)) ||
                   key == "pending_events_trigger_updating"; // 不需额外处理

    if (handled)
        config_ = new_config;

    return handled;
}

void default_event_handler::handle_config_event()
{
    bool do_refresh_index = false;
    bool run = true;

    if (g_async_queue_length(config_event_queue_) <= 0)
        return;

    spdlog::info("Handling config events...");

    while (run) {
        fs_event* event = (fs_event*)g_async_queue_pop(config_event_queue_);

        if (event->act == ACT_BLACKLIST_ADD_ABSOLUTE_PATH) {
            // add black list path
            std::string path = determine_blacklist_path(mount_info_, event->src);
            if (!path.empty()) {
                event_path_blocked_list_.emplace_back(path);

                // del index under the path
                recursive_update_index_delay(event->src, "");
                spdlog::info("Handled config event: ACT_BLACKLIST_ADD_ABSOLUTE_PATH, {}", event->src);
            } else {
                spdlog::info("Skipped config event: ACT_BLACKLIST_ADD_ABSOLUTE_PATH, {}", event->src);
            }
        } else if (event->act == ACT_BLACKLIST_DEL_ABSOLUTE_PATH) {
            // del black list path
            std::string path = determine_blacklist_path(mount_info_, event->src);
            if (!path.empty()) {
                for (auto it = event_path_blocked_list_.begin(); it != event_path_blocked_list_.end(); ++it) {
                    if (*it == path) {
                        event_path_blocked_list_.erase(it);
                        break;
                    }
                }

                // scan index under the path
                add_index_delay(event->src);
                scan_index_delay(event->src);
                spdlog::info("Handled config event: ACT_BLACKLIST_DEL_ABSOLUTE_PATH, {}", event->src);
            } else {
                spdlog::info("Skipped config event: ACT_BLACKLIST_DEL_ABSOLUTE_PATH, {}", event->src);
            }

        } else if (event->act == ACT_BLACKLIST_ADD_OTHER) {
            // add black list path
            std::string path = determine_blacklist_path(mount_info_, event->src);
            if (!path.empty()) {
                event_path_blocked_list_.emplace_back(path);

                // do refresh at last
                do_refresh_index = true;
                spdlog::info("Handled config event: ACT_BLACKLIST_ADD_OTHER, {}", event->src);
            } else {
                spdlog::info("Skipped config event: ACT_BLACKLIST_ADD_OTHER, {}", event->src);
            }
        } else {
            // ACT_CONFIG_EVENT_END
            run = false;
            spdlog::info("Handled config event: ACT_CONFIG_EVENT_END");
        }

        g_slice_free(fs_event, event);
    }

    if (do_refresh_index) {
        refresh_indexes();
        spdlog::info("Refreshed indexes");
    }

    spdlog::info("Handled config events");
}

ANYTHING_NAMESPACE_END
