#include "fsearch_folder_monitor_fanotify.h"

#include "fsearch_database_entry.h"
#include "fsearch_folder_monitor_event.h"
#include "fsearch_main_context_utils.h"

#include <config.h>
#include <fcntl.h>
#include <glib-unix.h>
#include <glib.h>
#include <linux/fanotify.h>
#include <stddef.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <sys/fanotify.h>
#include <sys/statfs.h>
#include <sys/types.h>
#include <sys/vfs.h>
#include <unistd.h>

#define FANOTIFY_FOLDER_MASK                                                                                           \
    (FAN_CREATE | FAN_CLOSE_WRITE | FAN_ATTRIB | FAN_DELETE | FAN_DELETE_SELF | FAN_MOVED_TO | FAN_MOVED_FROM          \
     | FAN_MOVE_SELF | FAN_EVENT_ON_CHILD | FAN_ONDIR)

struct FsearchFolderMonitorFanotify {
    GHashTable *handles_to_folders;
    GHashTable *folders_to_handles;

    GSource *monitor_source;
    GMainContext *monitor_context;

    GAsyncQueue *event_queue;

    int32_t fd;

    ssize_t file_handle_payload;

    GMutex mutex;
};

// From tracker-miners:
// https://gitlab.gnome.org/GNOME/tracker-miners/-/blob/master/src/miners/fs/tracker-monitor-fanotify.c
/* Binary compatible with the last portions of fanotify_event_info_fid */
typedef struct {
    fsid_t fsid;
    struct file_handle handle;
} FsearchDatabaseIndexHandleData;

static inline GBytes *
create_bytes_for_static_handle(FsearchDatabaseIndexHandleData *handle) {
    return g_bytes_new_static(handle, sizeof(FsearchDatabaseIndexHandleData) + handle->handle.handle_bytes);
}

static inline GBytes *
create_bytes_for_handle(FsearchDatabaseIndexHandleData *handle) {
    return g_bytes_new_take(handle, sizeof(FsearchDatabaseIndexHandleData) + handle->handle.handle_bytes);
}

typedef struct {
    int32_t flag;
    char present_symbol;
    char absent_symbol;
} FanotifyFlag;

/*
static const FanotifyFlag FANOTIFY_FLAGS[] = {{FAN_CREATE, 'c', '-'},
                                              {FAN_CLOSE_WRITE, 'w', '-'},
                                              {FAN_ATTRIB, 'a', '-'},
                                              {FAN_DELETE, 'd', '-'},
                                              {FAN_DELETE_SELF, 'D', '-'},
                                              {FAN_MOVED_TO, 'm', '-'},
                                              {FAN_MOVED_FROM, 'M', '-'},
                                              {FAN_MOVE_SELF, 'S', '-'},
                                              {FAN_EVENT_ON_CHILD, 'o', '-'},
                                              {FAN_ONDIR, '+', '-'}};

static void
print_fanotify_mask(uint32_t mask) {
    const size_t num_flags = G_N_ELEMENTS(FANOTIFY_FLAGS);

    for (size_t i = 0; i < num_flags; i++) {
        uint8_t symbol = (mask & FANOTIFY_FLAGS[i].flag) ? FANOTIFY_FLAGS[i].present_symbol
                                                         : FANOTIFY_FLAGS[i].absent_symbol;
        g_print("%c", symbol);
    }
}
*/

static bool
has_multiple_create_delete_events(uint32_t mask) {
    uint32_t num_changes = 0;
    if (mask & FAN_CREATE) {
        num_changes++;
    }
    if (mask & FAN_DELETE) {
        num_changes++;
    }
    if (mask & FAN_MOVED_FROM) {
        num_changes++;
    }
    if (mask & FAN_MOVED_TO) {
        num_changes++;
    }
    return num_changes > 1 ? true : false;
}

static void
queue_monitor_event(GAsyncQueue *event_queue, const char *file_name, GBytes *fid_bytes, int event_type, bool is_dir) {
    // fid_bytes needs to be copied to survive this call
    GBytes *owned_handle = g_bytes_new(g_bytes_get_data(fid_bytes, NULL), g_bytes_get_size(fid_bytes));
    g_async_queue_push(
        event_queue,
        fsearch_folder_monitor_event_new(file_name, owned_handle, event_type, FSEARCH_FOLDER_MONITOR_FANOTIFY, is_dir));
}

static gboolean
fanotify_listener_cb(int fd, GIOCondition condition, gpointer user_data) {
    FsearchFolderMonitorFanotify *self = user_data;

    // Assert that this function is run in the right monitor thread
    g_assert(g_main_context_is_owner(self->monitor_context));

    struct fanotify_event_metadata buf[2048] = {};

    for (;;) {
        ssize_t len = read(fd, buf, sizeof(buf));
        if (len == -1 && errno != EAGAIN) {
            g_debug("[fanotify_listener] failed to read from fd!");
            return G_SOURCE_REMOVE;
        }

        /* Check if end of available data reached. */
        if (len <= 0) {
            return G_SOURCE_CONTINUE;
        }

        for (const struct fanotify_event_metadata *metadata = (struct fanotify_event_metadata *)buf;
             FAN_EVENT_OK(metadata, len);
             metadata = FAN_EVENT_NEXT(metadata, len)) {
            /* Check that run-time and compile-time structures match. */
            if (metadata->vers != FANOTIFY_METADATA_VERSION) {
                g_warning("[fanotify_listener] fanotify ABI mismatch, monitoring is disabled");
                return G_SOURCE_REMOVE;
            }

            if (metadata->fd != FAN_NOFD) {
                g_warning("[fanotify_listener] fd is not FAN_NOFD\n");
                continue;
            }

            struct fanotify_event_info_fid *fid = (struct fanotify_event_info_fid *)(metadata + 1);
            struct file_handle *file_handle = (struct file_handle *)fid->handle;

            /* Ensure that the event info is of the correct type. */
            g_assert(fid->hdr.info_type == FAN_EVENT_INFO_TYPE_DFID_NAME);

            /* fsid/handle portions are compatible with HandleData */
            FsearchDatabaseIndexHandleData *handle = (FsearchDatabaseIndexHandleData *)&fid->fsid;
            g_autoptr(GBytes) fid_bytes = create_bytes_for_static_handle(handle);

            // Membership check only -- never dereference an entry off this thread.
            g_mutex_lock(&self->mutex);
            const bool is_watched = g_hash_table_contains(self->handles_to_folders, fid_bytes);
            g_mutex_unlock(&self->mutex);

            const char *file_name = (const char *)(file_handle->f_handle + file_handle->handle_bytes);
            if (g_strcmp0(file_name, ".") == 0) {
                file_name = NULL;
            }

            // print_fanotify_mask(metadata->mask);
            // g_print(": %s\n", file_name ? file_name : "UNKNOWN");

            if (!is_watched) {
                g_debug("[fanotify_listener] no watched entry for handle found: %llu -> %s",
                        metadata->mask,
                        file_name ? file_name : ".");
                continue;
            }

            const bool is_dir = metadata->mask & FAN_ONDIR ? true : false;
            bool skip_attrib = false;

            // Always push MOVE_SELF and DELETE_SELF
            if (metadata->mask & FAN_MOVE_SELF) {
                skip_attrib = true;
                queue_monitor_event(self->event_queue,
                                    file_name,
                                    fid_bytes,
                                    FSEARCH_FOLDER_MONITOR_EVENT_MOVE_SELF,
                                    is_dir);
            }
            if (metadata->mask & FAN_DELETE_SELF) {
                skip_attrib = true;
                queue_monitor_event(self->event_queue,
                                    file_name,
                                    fid_bytes,
                                    FSEARCH_FOLDER_MONITOR_EVENT_DELETE_SELF,
                                    is_dir);
            }
            if (has_multiple_create_delete_events(metadata->mask)) {
                // There's no way to know in which order those events happened, hence we must do a rescan.
                g_print("multiple create/delete events: %s\n", file_name ? file_name : ".");
                queue_monitor_event(self->event_queue, file_name, fid_bytes, FSEARCH_FOLDER_MONITOR_EVENT_RESCAN, is_dir);
                // We can skip other events in the same mask here, since we're going to rescan the file/folder anyway
                continue;
            }

            if (metadata->mask & FAN_CREATE) {
                skip_attrib = true;
                queue_monitor_event(self->event_queue, file_name, fid_bytes, FSEARCH_FOLDER_MONITOR_EVENT_CREATE, is_dir);
            }
            if (metadata->mask & FAN_DELETE) {
                skip_attrib = true;
                queue_monitor_event(self->event_queue, file_name, fid_bytes, FSEARCH_FOLDER_MONITOR_EVENT_DELETE, is_dir);
            }
            if (metadata->mask & FAN_MOVED_FROM) {
                skip_attrib = true;
                queue_monitor_event(self->event_queue,
                                    file_name,
                                    fid_bytes,
                                    FSEARCH_FOLDER_MONITOR_EVENT_MOVED_FROM,
                                    is_dir);
            }
            if (metadata->mask & FAN_MOVED_TO) {
                skip_attrib = true;
                queue_monitor_event(self->event_queue,
                                    file_name,
                                    fid_bytes,
                                    FSEARCH_FOLDER_MONITOR_EVENT_MOVED_TO,
                                    is_dir);
            }
            if (!skip_attrib) {
                if (metadata->mask & FAN_ATTRIB) {
                    queue_monitor_event(self->event_queue,
                                        file_name,
                                        fid_bytes,
                                        FSEARCH_FOLDER_MONITOR_EVENT_ATTRIB,
                                        is_dir);
                }
                else if (metadata->mask & FAN_CLOSE_WRITE) {
                    queue_monitor_event(self->event_queue,
                                        file_name,
                                        fid_bytes,
                                        FSEARCH_FOLDER_MONITOR_EVENT_CLOSE_WRITE,
                                        is_dir);
                }
            }
        }
    }

    return G_SOURCE_REMOVE;
}

static void
unwatch_folder_cb(gpointer key, gpointer value, gpointer user_data);

static void
unwatch_all(FsearchFolderMonitorFanotify *self) {
    g_autoptr(GMutexLocker) locker = g_mutex_locker_new(&self->mutex);
    g_assert_nonnull(locker);

    g_hash_table_foreach(self->handles_to_folders, (GHFunc)unwatch_folder_cb, self);
    g_hash_table_remove_all(self->folders_to_handles);
    g_hash_table_remove_all(self->handles_to_folders);
}

FsearchFolderMonitorFanotify *
fsearch_folder_monitor_fanotify_new(GMainContext *monitor_context, GAsyncQueue *event_queue) {
    g_return_val_if_fail(monitor_context, NULL);
    g_return_val_if_fail(event_queue, NULL);

    g_autofree FsearchFolderMonitorFanotify *self = calloc(1, sizeof(FsearchFolderMonitorFanotify));

    self->fd = fanotify_init(FAN_CLOEXEC | FAN_NONBLOCK | FAN_CLASS_NOTIF | FAN_REPORT_DFID_NAME, O_RDONLY);
    if (self->fd < 0) {
        return NULL;
    }

    self->event_queue = g_async_queue_ref(event_queue);

    self->handles_to_folders = g_hash_table_new_full(g_bytes_hash, g_bytes_equal, (GDestroyNotify)g_bytes_unref, NULL);
    self->folders_to_handles = g_hash_table_new_full(g_direct_hash, g_direct_equal, NULL, (GDestroyNotify)g_bytes_unref);

    self->monitor_context = monitor_context;

    self->monitor_source = g_unix_fd_source_new(self->fd, G_IO_IN | G_IO_ERR | G_IO_HUP);
    g_source_set_callback(self->monitor_source, (GSourceFunc)fanotify_listener_cb, self, NULL);
    g_source_attach(self->monitor_source, self->monitor_context);

    g_mutex_init(&self->mutex);

    return g_steal_pointer(&self);
}

void
fsearch_folder_monitor_fanotify_free(FsearchFolderMonitorFanotify *self) {
    g_return_if_fail(self);

    if (self->monitor_source) {
        fsearch_main_context_blocking_call(self->monitor_context,
                                           (FsearchMainContextFunc)g_source_destroy,
                                           self->monitor_source);
    }
    g_clear_pointer(&self->monitor_source, g_source_unref);
    unwatch_all(self);
    g_clear_pointer(&self->folders_to_handles, g_hash_table_unref);
    g_clear_pointer(&self->handles_to_folders, g_hash_table_unref);

    if (self->fd >= 0) {
        close(self->fd);
    }

    g_clear_pointer(&self->event_queue, g_async_queue_unref);

    g_mutex_clear(&self->mutex);

    g_clear_pointer(&self, free);
}

bool
fsearch_folder_monitor_fanotify_watch(FsearchFolderMonitorFanotify *self, FsearchDatabaseEntry *folder, const char *path) {
    g_assert(folder != NULL);
    struct statfs buf;
    if (statfs(path, &buf) < 0) {
        if (errno != ENOENT)
            g_warning("Could not get filesystem ID for %s", path);
        return false;
    }

    g_autofree FsearchDatabaseIndexHandleData *handle_data = calloc(1,
                                                                    sizeof(FsearchDatabaseIndexHandleData)
                                                                        + self->file_handle_payload);

    while (true) {
        int32_t mntid = -1;
        if (name_to_handle_at(AT_FDCWD, path, (void *)&handle_data->handle, &mntid, 0) < 0) {
            if (errno == EOVERFLOW) {
                /* The payload is not big enough to hold a file_handle,
                 * in this case we get the ideal handle data size, so
                 * fetch that and retry.
                 */
                self->file_handle_payload = handle_data->handle.handle_bytes;
                g_clear_pointer(&handle_data, free);
                handle_data = calloc(1, sizeof(FsearchDatabaseIndexHandleData) + self->file_handle_payload);
                handle_data->handle.handle_bytes = self->file_handle_payload;
                continue;
            }
            else if (errno != ENOENT) {
                g_warning("Could not get file handle for '%s': %m", path);
            }
            return false;
        }
        break;
    }

    memcpy(&handle_data->fsid, &buf.f_fsid, sizeof(fsid_t));

    g_autoptr(GBytes) handle_bytes = create_bytes_for_handle(g_steal_pointer(&handle_data));

    // To avoid a potential race condition, we first add the folder-to-handle associations to the hash tables
    // and only then add the fanotify mark to monitor the folder.

    g_autoptr(GMutexLocker) locker = g_mutex_locker_new(&self->mutex);
    g_assert_nonnull(locker);

    g_hash_table_insert(self->handles_to_folders, g_bytes_ref(handle_bytes), folder);
    g_hash_table_insert(self->folders_to_handles, folder, g_bytes_ref(handle_bytes));
    if (!fanotify_mark(self->fd, FAN_MARK_ADD | FAN_MARK_ONLYDIR, FANOTIFY_FOLDER_MASK, AT_FDCWD, path)) {
        db_entry_set_monitored_fanotify(folder);
        return true;
    }
    // Failed to monitor the folder -> remove the associations from the hash tables
    g_hash_table_remove(self->handles_to_folders, handle_bytes);
    g_hash_table_remove(self->folders_to_handles, folder);
    return false;
}

static void
unwatch_folder(FsearchFolderMonitorFanotify *self, FsearchDatabaseEntry *folder) {
    g_autoptr(GString) path_full = db_entry_get_path_full(folder);
    if (fanotify_mark(self->fd, FAN_MARK_REMOVE, FANOTIFY_FOLDER_MASK, AT_FDCWD, path_full->str)) {
        if (errno != ENOENT) {
            g_debug("[unwatch_folder] failed to remove fanotify mark: %s", path_full->str);
        }
    }
    db_entry_set_unmonitored_fanotify(folder);
}

static void
unwatch_folder_cb(gpointer key, gpointer value, gpointer user_data) {
    FsearchFolderMonitorFanotify *self = user_data;
    FsearchDatabaseEntry *folder = value;
    unwatch_folder(self, folder);
}

void
fsearch_folder_monitor_fanotify_unwatch(FsearchFolderMonitorFanotify *self, FsearchDatabaseEntry *folder) {
    GBytes *fanotify_handle_bytes = g_hash_table_lookup(self->folders_to_handles, folder);
    if (fanotify_handle_bytes) {
        g_autoptr(GMutexLocker) locker = g_mutex_locker_new(&self->mutex);
        g_assert_nonnull(locker);

        unwatch_folder(self, folder);
        g_hash_table_remove(self->handles_to_folders, fanotify_handle_bytes);
        g_hash_table_remove(self->folders_to_handles, folder);
    }
    else {
        g_autoptr(GString) path_full = db_entry_get_path_full(folder);
        g_debug("[unwatch_folder] no fanotify handle found for folder: %s", path_full->str);
    }
}

FsearchDatabaseEntry *
fsearch_folder_monitor_fanotify_resolve(FsearchFolderMonitorFanotify *self, gpointer handle) {
    g_return_val_if_fail(self, NULL);
    g_autoptr(GMutexLocker) locker = g_mutex_locker_new(&self->mutex);
    g_assert_nonnull(locker);
    return g_hash_table_lookup(self->handles_to_folders, handle);
}