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

#define _GNU_SOURCE
#include "cleanup.h"
#include "dconfig.h"
#include "log.h"
#include "netlink_client.h"
#include "service.h"

#include <systemd/sd-journal.h>
#include <utmp.h>

#include <limits.h>
#include <pthread.h>
#include <pwd.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/uio.h> // for struct iovec

struct err_notify_state *global_state = NULL;
static sd_bus_slot *g_vtable_slot = NULL;

/* Get username from UID */
static void get_username_from_uid(uid_t uid, char *buffer, size_t buffer_size)
{
    struct passwd pwd;
    struct passwd *result;
    char pwd_buf[1024];
    int s = getpwuid_r(uid, &pwd, pwd_buf, sizeof(pwd_buf), &result);
    if (s == 0 && result != NULL) {
        snprintf(buffer, buffer_size, "%s", pwd.pw_name);
    } else {
        // If failed, leave buffer as empty string
        buffer[0] = '\0';
        if (s != 0) {
            log_warning("getpwuid_r failed for UID %u: %s", (unsigned int)uid, strerror(s));
        }
    }
}

/* Build comma-separated file paths list from filenames array
 * Returns: newly allocated string with comma-separated paths, or NULL on error
 * Caller must free the returned string
 */
static char *build_paths_list(const char **filenames, int count)
{
    if (count <= 0 || !filenames)
        return NULL;

    size_t total_len = 0;
    for (int i = 0; i < count; i++) {
        total_len += strlen(filenames[i]);
        if (i > 0)
            total_len += 2; /* for ", " */
    }

    char *paths_list = malloc(total_len + 1);
    if (!paths_list)
        return NULL;

    paths_list[0] = '\0';
    for (int i = 0; i < count; i++) {
        if (i > 0)
            strcat(paths_list, ", ");
        strcat(paths_list, filenames[i]);
    }

    return paths_list;
}

/* Free watch directories */
static void free_watch_dirs(void)
{
    pthread_mutex_lock(&g_watch_dirs_mutex);

    if (g_watch_dirs) {
        dconfig_free_string_array(g_watch_dirs, g_watch_dirs_count);
        g_watch_dirs = NULL;
        g_watch_dirs_count = 0;
    }

    pthread_mutex_unlock(&g_watch_dirs_mutex);
}

/* Check if path is in watch directories */
static bool is_path_in_watch_dirs(const char *path)
{
    if (!path) {
        return false;
    }

    pthread_mutex_lock(&g_watch_dirs_mutex);

    bool result = false;
    for (size_t i = 0; i < g_watch_dirs_count; i++) {
        const char *watch_dir = g_watch_dirs[i];
        size_t watch_dir_len = strlen(watch_dir);

        // Check if path starts with watch_dir
        // and the next character is '/' or '\0'
        if (strncmp(path, watch_dir, watch_dir_len) == 0) {
            if (path[watch_dir_len] == '/' || path[watch_dir_len] == '\0') {
                result = true;
                break;
            }
        }
    }

    pthread_mutex_unlock(&g_watch_dirs_mutex);
    return result;
}

/* Blacklist patterns to filter out */
static const char *blacklist_patterns[] = {
    "/__pycache__",
    NULL // Sentinel
};

/**
 * Check if a path matches any blacklist pattern
 * @param path The file path to check
 * @return true if path should be filtered out, false otherwise
 */
static bool is_path_blacklisted(const char *path)
{
    if (!path)
        return false;

    for (int i = 0; blacklist_patterns[i] != NULL; i++) {
        if (strstr(path, blacklist_patterns[i]) != NULL) {
            return true;
        }
    }
    return false;
}

/**
 * Check if notification should be processed based on monitored directories and blacklist
 * @param data The notification data containing file paths
 * @return true if notification should be processed, false otherwise
 */
static bool should_process_notification(const struct err_notify_data *data)
{
    if (!data || data->inode_count == 0)
        return false;

    bool has_monitored_file = false;
    for (int i = 0; i < data->inode_count; i++) {
        const char *path = data->filenames[i];

        // Skip blacklisted paths
        if (is_path_blacklisted(path)) {
            log_debug("Path '%s' is blacklisted, skipping", path);
            continue;
        }

        // Check if path is in monitored directories
        if (is_path_in_watch_dirs(path)) {
            has_monitored_file = true;
            break;
        }
    }

    if (!has_monitored_file) {
        log_debug("No monitored files in notification, skipping");
    }

    return has_monitored_file;
}

/* Callback for netlink error notifications */
static void error_notification_callback(const struct err_notify_data *data, void *userdata)
{
    (void)userdata;

    if (!data)
        return;

    if (data->pid <= 0) {
        log_warning("Invalid pid in callback: %d", data->pid);
        return;
    }

    if (!should_process_notification(data)) {
        return;
    }

    /* Data acquisition section */
    char cgroup_buffer[PATH_MAX] = { 0 };
    get_process_cgroup(data->pid, cgroup_buffer, sizeof(cgroup_buffer));

    char cmdline_buffer[256] = { 0 };
    get_process_cmdline(data->pid, cmdline_buffer, sizeof(cmdline_buffer), data->comm);

    emit_error_occurred_signal(data, cgroup_buffer, cmdline_buffer);

    char username_buffer[UT_NAMESIZE] = { 0 };
    get_username_from_uid(data->uid, username_buffer, sizeof(username_buffer));

    /* Declare variables with automatic cleanup (must be initialized to NULL) */
    _cleanup_free_ char *paths_list = NULL;
    _cleanup_free_ char *message_field = NULL;
    _cleanup_free_ char *pid_field = NULL;
    _cleanup_free_ char *ppid_field = NULL;
    _cleanup_free_ char *cmdline_field = NULL;
    _cleanup_free_ char *cgroup_field = NULL;
    _cleanup_free_ char *uid_field = NULL;
    _cleanup_free_ char *gid_field = NULL;
    _cleanup_free_ char *username_field = NULL;
    _cleanup_free_ char *comm_field = NULL;

    /* Dynamic file path array (using calloc to ensure NULL-terminated) */
    _cleanup_strv_ char **dynamic_strs = calloc(data->inode_count + 1, sizeof(char *));

    /* iovec array */
    int fixed_fields = 11;
    int total_fields = fixed_fields + data->inode_count;
    _cleanup_free_ struct iovec *iov = malloc(total_fields * sizeof(struct iovec));

    /* Memory allocation check */
    if (!iov || !dynamic_strs)
        return;

    /* Build comma-separated file paths list */
    paths_list = build_paths_list(data->filenames, data->inode_count);

    /* Populate fields */
    if (asprintf(&message_field,
                 "MESSAGE=write operation failed because filesystem is read-only, "
                 "path=%s, pid=%d, uid=%u, username=%s, cgroup=%s, cmdline=%s",
                 paths_list ? paths_list : "(unknown)",
                 (int)data->pid,
                 (unsigned int)data->uid,
                 username_buffer[0] ? username_buffer : "(unknown)",
                 cgroup_buffer[0] ? cgroup_buffer : "(unknown)",
                 cmdline_buffer) < 0)
        return;
    iov[0].iov_base = message_field;
    iov[0].iov_len = strlen(message_field);

    iov[1].iov_base = "PRIORITY=4";
    iov[1].iov_len = strlen(iov[1].iov_base);

    iov[2].iov_base = "SYSLOG_IDENTIFIER=" SYSLOG_IDENTIFIER;
    iov[2].iov_len = strlen(iov[2].iov_base);

    if (asprintf(&pid_field, "PID=%d", (int)data->pid) < 0)
        return;
    iov[3].iov_base = pid_field;
    iov[3].iov_len = strlen(pid_field);

    if (asprintf(&ppid_field, "PPID=%d", (int)data->ppid) < 0)
        return;
    iov[4].iov_base = ppid_field;
    iov[4].iov_len = strlen(ppid_field);

    if (asprintf(&comm_field, "COMM=%s", data->comm ? data->comm : "") < 0)
        return;
    iov[5].iov_base = comm_field;
    iov[5].iov_len = strlen(comm_field);

    if (asprintf(&cmdline_field, "CMDLINE=%s", cmdline_buffer[0] ? cmdline_buffer : "(null)") < 0)
        return;
    iov[6].iov_base = cmdline_field;
    iov[6].iov_len = strlen(cmdline_field);

    if (asprintf(&cgroup_field, "CGROUP=%s", cgroup_buffer[0] ? cgroup_buffer : "(null)") < 0)
        return;
    iov[7].iov_base = cgroup_field;
    iov[7].iov_len = strlen(cgroup_field);

    if (asprintf(&uid_field, "UID=%u", (unsigned int)data->uid) < 0)
        return;
    iov[8].iov_base = uid_field;
    iov[8].iov_len = strlen(uid_field);

    if (asprintf(&username_field, "USERNAME=%s", username_buffer[0] ? username_buffer : "") < 0)
        return;
    iov[9].iov_base = username_field;
    iov[9].iov_len = strlen(username_field);

    if (asprintf(&gid_field, "GID=%u", (unsigned int)data->gid) < 0)
        return;
    iov[10].iov_base = gid_field;
    iov[10].iov_len = strlen(gid_field);

    /* Dynamic file path fields */
    for (int i = 0; i < data->inode_count; i++) {
        if (asprintf(&dynamic_strs[i], "TARGET_PATH=%s", data->filenames[i]) < 0)
            return;

        iov[fixed_fields + i].iov_base = dynamic_strs[i];
        iov[fixed_fields + i].iov_len = strlen(dynamic_strs[i]);
    }

    /* Send log */
    int r = sd_journal_sendv(iov, total_fields);
    if (r < 0)
        log_warning("Failed to send log: %s", strerror(-r));

    /* Function end, all _cleanup_ marked variables automatically call cleanup functions */
}

const sd_bus_vtable auditor_vtable[] = {
    SD_BUS_VTABLE_START(0),

    // Properties
    SD_BUS_WRITABLE_PROPERTY("Enabled",
                             "b",
                             property_get_enabled,
                             property_set_enabled,
                             0,
                             SD_BUS_VTABLE_UNPRIVILEGED | SD_BUS_VTABLE_PROPERTY_EMITS_CHANGE),

    // Methods
    SD_BUS_METHOD("ExportLog", "", "h", method_export_log, SD_BUS_VTABLE_UNPRIVILEGED),

    // Signals
    SD_BUS_SIGNAL("ErrorOccurred", "a{sv}", 0),

    SD_BUS_VTABLE_END
};

int DSMRegister(const char *name, void *data)
{
    (void)name;
    if (!data) {
        return -1;
    }

    sd_bus *bus = (sd_bus *)data;

    // Save the bus pointer for signal emission
    g_bus = bus;

    // Initialize rate limiting
    rate_limit_table_init(&g_rate_limit_table);

    if (sd_bus_add_object_vtable(bus,
                                 &g_vtable_slot,
                                 DBUS_OBJECT_PATH,
                                 DBUS_INTERFACE_NAME,
                                 auditor_vtable,
                                 NULL)
        < 0) {
        log_warning("Failed to add object vtable");
        return -1;
    }

    // Load configuration from dconfig
    bool tmp_enabled = true;
    char **tmp_watch_dirs = NULL;
    size_t tmp_watch_dirs_count = 0;

    if (dconfig_load_all_config(bus,
                                &tmp_enabled,
                                &tmp_watch_dirs,
                                &tmp_watch_dirs_count)
        >= 0) {
        // Update global configuration
        g_enabled = tmp_enabled;

        pthread_mutex_lock(&g_watch_dirs_mutex);
        g_watch_dirs = tmp_watch_dirs;
        g_watch_dirs_count = tmp_watch_dirs_count;
        pthread_mutex_unlock(&g_watch_dirs_mutex);

        log_info("Configuration loaded from dconfig");
    } else {
        log_warning("Failed to load configuration from dconfig, using defaults");
        // Use default values - already set in global variables
        // Set default watch directories
        pthread_mutex_lock(&g_watch_dirs_mutex);
        g_watch_dirs_count = 4;
        g_watch_dirs = calloc(g_watch_dirs_count + 1, sizeof(char *));
        if (g_watch_dirs) {
            g_watch_dirs[0] = strdup("/usr");
            g_watch_dirs[1] = strdup("/persistent/ostree");
            g_watch_dirs[2] = strdup("/sysroot/ostree");
            g_watch_dirs[3] = strdup("/ostree");
        } else {
            g_watch_dirs_count = 0;
        }
        pthread_mutex_unlock(&g_watch_dirs_mutex);
    }

    /* Initialize Generic Netlink */
    if (err_notify_init(&global_state) < 0) {
        log_warning("Failed to initialize error notification");
        return 0;
    }

    /* Register callback for error notifications */
    err_notify_set_callback(global_state, error_notification_callback, NULL);

    /* Join multicast group for event-driven processing */
    if (err_notify_join_multicast(global_state) < 0) {
        log_warning("Failed to join multicast group");
        err_notify_cleanup(global_state);
        global_state = NULL;
        return -1;
    }

    /* Enable or disable kernel notification based on config */
    if (g_enabled) {
        if (err_notify_enable(global_state) < 0) {
            log_warning("Failed to enable kernel notification");
        } else {
            log_info("Kernel notification enabled");
        }
    } else {
        if (err_notify_disable(global_state) < 0) {
            log_warning("Failed to disable kernel notification");
        } else {
            log_info("Kernel notification disabled");
        }
    }

    /* Initialize sd-event loop */
    if (event_loop_init(bus, global_state) < 0) {
        log_warning("Failed to initialize event loop");
        err_notify_cleanup(global_state);
        global_state = NULL;
        return -1;
    }

    log_info("Plugin initialized with sd-event successfully");
    return 0;
}


// This function is used for resource cleanup
// Non-resident plugins must implement this function to prevent memory leaks
int DSMUnRegister(const char *name, void *data)
{
    (void)name;
    (void)data;

    /* Stop the sd-event loop */
    event_loop_cleanup();

    /* Clean up netlink state */
    if (global_state) {
        err_notify_cleanup(global_state);
        global_state = NULL;
    }

    /* Clean up rate limiting */
    rate_limit_table_cleanup(&g_rate_limit_table);

    /* Free watch directories */
    free_watch_dirs();

    /* Unref D-Bus vtable slot */
    if (g_vtable_slot) {
        sd_bus_slot_unref(g_vtable_slot);
        g_vtable_slot = NULL;
    }

    /* Clear bus pointer */
    g_bus = NULL;

    log_info("Plugin cleanup completed");
    return 0;
}
