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

#define _GNU_SOURCE

#include "service.h"

#include "dconfig.h"
#include "log.h"
#include "netlink_client.h"
#include "polkit_auth.h"

#include <netlink/errno.h>
#include <systemd/sd-journal.h>

#include <errno.h>
#include <fcntl.h>
#include <pthread.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <time.h>
#include <unistd.h>

/* Global state */
sd_bus *g_bus = NULL;
bool g_enabled = true;              // Default: enabled
struct rate_limit_table g_rate_limit_table;

/* sd-event related */
sd_event *g_event = NULL;
sd_event_source *g_netlink_source = NULL;
pthread_t g_event_thread = 0;
static bool g_event_thread_running = false;

/* Watch directories */
char **g_watch_dirs = NULL;
size_t g_watch_dirs_count = 0;
pthread_mutex_t g_watch_dirs_mutex = PTHREAD_MUTEX_INITIALIZER;

/* Get main PID from cgroup.procs file
 * Returns main PID (positive pid_t) on success, -1 on failure */
static pid_t get_main_pid_from_cgroup(const char *cgroup)
{
    if (!cgroup)
        return -1;

    /* Build cgroup.procs file path: /sys/fs/cgroup/{cgroup}/cgroup.procs */
    gchar *procs_path = g_strdup_printf("/sys/fs/cgroup%s/cgroup.procs", cgroup);
    if (!procs_path)
        return -1;

    /* Read the file content */
    gchar *contents = NULL;
    gsize length = 0;
    GError *error = NULL;

    if (!g_file_get_contents(procs_path, &contents, &length, &error)) {
        if (error) {
            log_debug("Failed to read %s: %s", procs_path, error->message);
            g_error_free(error);
        }
        g_free(procs_path);
        return -1;
    }

    g_free(procs_path);

    /* Parse first PID from the file */
    pid_t pid = -1;
    if (contents && length > 0) {
        gchar **lines = g_strsplit(contents, "\n", -1);
        if (lines && lines[0]) {
            gchar *first_line = g_strstrip(lines[0]);
            if (strlen(first_line) > 0) {
                long parsed_pid = strtol(first_line, NULL, 10);
                if (parsed_pid > 0)
                    pid = (pid_t)parsed_pid;
            }
        }
        g_strfreev(lines);
    }
    g_free(contents);

    return pid;
}

/* Per-key rate limiting implementation using GHashTable
 * Constructs a unique rate limit key from cgroup and cmdline.
 * Returns a newly allocated string that must be freed with g_free(),
 * or NULL if neither cgroup nor cmdline is available. */
static char *make_rate_limit_key(const char *cgroup, const char *cmdline)
{
    char *normalized_cgroup = NULL;
    char *normalized_cmd = NULL;
    char *key = NULL;

    /* Normalize cgroup */
    if (cgroup) {
        if (strstr(cgroup, "/app.slice/")) {
            /* If cgroup contains /app.slice/, use it as-is */
            normalized_cgroup = g_strdup(cgroup);
        } else {
            /* Try to get main PID from cgroup.procs */
            pid_t main_pid = get_main_pid_from_cgroup(cgroup);
            if (main_pid > 0) {
                normalized_cgroup = g_strdup_printf("%s,mainPid=%d", cgroup, (int)main_pid);
            } else {
                /* Failed to get MainPID, use cgroup as-is */
                normalized_cgroup = g_strdup(cgroup);
            }
        }
    }

    /* Use cmdline only when normalized_cgroup is empty */
    if (normalized_cgroup) {
        /* cgroup is available, use it as the key */
        key = g_strdup(normalized_cgroup);
    } else if (cmdline) {
        /* No cgroup, normalize cmdline: only keep the first token before space so
         * the key is shared by the same executable regardless of arguments. */
        const char *space = strchr(cmdline, ' ');
        if (space)
            normalized_cmd = g_strndup(cmdline, space - cmdline);
        else
            normalized_cmd = g_strdup(cmdline);
        key = g_strdup(normalized_cmd);
    } else {
        /* Neither cgroup nor cmdline available */
        key = NULL;
    }

    g_free(normalized_cgroup);
    g_free(normalized_cmd);

    return key;
}

static void rate_limit_entry_free(gpointer data)
{
    g_free(data);
}

void rate_limit_table_init(struct rate_limit_table *table)
{
    table->table = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, rate_limit_entry_free);
    table->check_count = 0;
    pthread_mutex_init(&table->mutex, NULL);
}

bool rate_limit_table_check(struct rate_limit_table *table, const char *cgroup, const char *cmdline)
{
    bool result = false;
    bool need_gc = false;

    pthread_mutex_lock(&table->mutex);

    /* Check if gc is needed */
    table->check_count++;
    if (table->check_count >= RATE_LIMIT_GC_INTERVAL) {
        table->check_count = 0;
        need_gc = true;
    }

    char *key = make_rate_limit_key(cgroup, cmdline);
    if (!key)
        goto out;

    log_debug("rate limit key: %s", key);

    time_t now = time(NULL);
    struct rate_limit_entry *entry = g_hash_table_lookup(table->table, key);

    if (!entry) {
        entry = g_new0(struct rate_limit_entry, 1);
        entry->window_start = now;
        entry->last_access = now;
        entry->count = 1;
        g_hash_table_insert(table->table, key, entry);
        log_debug("rate limit key inserted: %s", key);
        result = true;
        goto out;
    }

    g_free(key);
    entry->last_access = now;

    /* Reset window if it has passed */
    if (now - entry->window_start >= RATE_LIMIT_WINDOW_SECONDS) {
        entry->window_start = now;
        entry->count = 1;
        result = true;
        goto out;
    }

    if (entry->count >= RATE_LIMIT_MAX_COUNT) {
        result = false;
        goto out;
    }

    entry->count++;
    result = true;

out:
    pthread_mutex_unlock(&table->mutex);

    /* Run gc after releasing lock */
    if (need_gc)
        rate_limit_table_gc(table);

    return result;
}

void rate_limit_table_gc(struct rate_limit_table *table)
{
    log_debug("Running rate limit table GC");
    pthread_mutex_lock(&table->mutex);

    time_t now = time(NULL);
    GHashTableIter iter;
    gpointer key, value;
    GList *to_remove = NULL;

    g_hash_table_iter_init(&iter, table->table);
    while (g_hash_table_iter_next(&iter, &key, &value)) {
        struct rate_limit_entry *entry = value;
        if (now - entry->last_access >= RATE_LIMIT_ENTRY_EXPIRE_SECONDS)
            to_remove = g_list_prepend(to_remove, key);
    }

    for (GList *l = to_remove; l; l = l->next)
        g_hash_table_remove(table->table, l->data);

    g_list_free(to_remove);
    pthread_mutex_unlock(&table->mutex);
}

void rate_limit_table_cleanup(struct rate_limit_table *table)
{
    pthread_mutex_lock(&table->mutex);
    g_hash_table_destroy(table->table);
    table->table = NULL;
    pthread_mutex_unlock(&table->mutex);
    pthread_mutex_destroy(&table->mutex);
}

/* Property handlers */
int property_get_enabled(sd_bus *bus,
                         const char *path,
                         const char *interface,
                         const char *property,
                         sd_bus_message *reply,
                         void *userdata,
                         sd_bus_error *ret_error)
{
    (void)bus;
    (void)path;
    (void)interface;
    (void)property;
    (void)userdata;
    (void)ret_error;

    return sd_bus_message_append(reply, "b", g_enabled);
}

int property_set_enabled(sd_bus *bus,
                         const char *path,
                         const char *interface,
                         const char *property,
                         sd_bus_message *value,
                         void *userdata,
                         sd_bus_error *ret_error)
{
    (void)userdata;

    // Check polkit authorization
    const char *sender = sd_bus_message_get_sender(value);
    if (!sender) {
        sd_bus_error_set_const(ret_error, SD_BUS_ERROR_FAILED, "Failed to get message sender");
        log_warning("property_set_enabled: failed to get D-Bus sender");
        return -EPERM;
    }

    if (!polkit_check_authorization(sender, POLKIT_ACTION_SET_ENABLED)) {
        sd_bus_error_set_const(ret_error,
                               SD_BUS_ERROR_ACCESS_DENIED,
                               "Authorization required to modify enabled property");
        log_warning("property_set_enabled: authorization denied for sender %s", sender);
        return -EPERM;
    }

    int enabled;
    int r = sd_bus_message_read(value, "b", &enabled);
    if (r < 0) {
        sd_bus_error_set_errno(ret_error, -r);
        return r;
    }

    // Only proceed if the value actually changed
    if ((bool)enabled == g_enabled) {
        return 0; // Value unchanged, nothing to do
    }

    g_enabled = enabled;

    // Control kernel notification switch
    if (global_state) {
        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");
            }
        }
    }

    // Sync to dconfig
    if (dconfig_sync_bool(g_bus, "enabled", g_enabled) < 0) {
        log_warning("Failed to sync enabled to dconfig");
    }

    // Emit PropertiesChanged signal only when value changed
    r = sd_bus_emit_properties_changed(bus, path, interface, property, NULL);
    if (r < 0) {
        log_warning("Failed to emit PropertiesChanged signal: %s", strerror(-r));
    }

    return 0;
}

/* ExportLog method implementation */
int method_export_log(sd_bus_message *m, void *userdata, sd_bus_error *ret_error)
{
    (void)userdata;

    // Check polkit authorization
    const char *sender = sd_bus_message_get_sender(m);
    if (!sender) {
        sd_bus_error_set_const(ret_error, SD_BUS_ERROR_FAILED, "Failed to get message sender");
        log_warning("ExportLog: failed to get D-Bus sender");
        return -EPERM;
    }

    if (!polkit_check_authorization(sender, POLKIT_ACTION_EXPORT_LOG)) {
        sd_bus_error_set_const(ret_error,
                               SD_BUS_ERROR_ACCESS_DENIED,
                               "Authorization required to export logs");
        log_warning("ExportLog: authorization denied for sender %s", sender);
        return -EPERM;
    }

    sd_journal *journal = NULL;
    int fd = -1;
    char temp_path[] = "/tmp/deepin-err-notify-log-XXXXXX";
    int r;

    // Open the systemd journal
    r = sd_journal_open(&journal, SD_JOURNAL_LOCAL_ONLY);
    if (r < 0) {
        sd_bus_error_set_errno(ret_error, -r);
        log_warning("Failed to open journal: %s", strerror(-r));
        goto cleanup;
    }

    // Set AND filter conditions
    // Condition 1: Service unit name
    sd_journal_add_match(journal, "_SYSTEMD_UNIT=deepin-service-manager.service", 0);
    // Condition 2: Syslog identifier (AND relationship with the above condition)
    sd_journal_add_match(journal, "SYSLOG_IDENTIFIER=" SYSLOG_IDENTIFIER, 0);

    // Calculate one week ago timestamp for filtering
    struct timespec ts;
    uint64_t start_usec; // One week ago timestamp

    r = clock_gettime(CLOCK_REALTIME, &ts);
    if (r < 0) {
        sd_bus_error_set_errno(ret_error, errno);
        log_warning("Failed to get current time: %s", strerror(errno));
        r = -errno;
        goto cleanup;
    }

    uint64_t now_usec = (uint64_t)ts.tv_sec * 1000000ULL + (ts.tv_nsec / 1000);
    uint64_t one_week_usec = 7ULL * 24 * 3600 * 1000000ULL;

    if (now_usec <= one_week_usec) {
        sd_bus_error_set_const(ret_error,
                               SD_BUS_ERROR_FAILED,
                               "Invalid system time (too early)");
        log_warning("Invalid system time: %lu usec (earlier than 1970-01-08)", now_usec);
        r = -EINVAL;
        goto cleanup;
    }

    start_usec = now_usec - one_week_usec;

    // Seek to the end (most recent) for reverse iteration
    sd_journal_seek_tail(journal);

    // Create a temporary file to write the log
    fd = mkstemp(temp_path);
    if (fd < 0) {
        sd_bus_error_set_errno(ret_error, errno);
        log_warning("Failed to create temporary file: %s", strerror(errno));
        r = -errno;
        goto cleanup;
    }

    // Unlink the file immediately (it will be deleted when fd is closed)
    unlink(temp_path);

    // Write journal entries to the file
    const void *data;
    size_t length;
    int count = 0;

    // Outer loop: iterate through journal entries in reverse order (newest first)
    while (sd_journal_previous(journal) > 0) {
        // Check if entry is older than one week
        uint64_t entry_usec;
        if (sd_journal_get_realtime_usec(journal, &entry_usec) == 0) {
            if (entry_usec < start_usec) {
                break; // Stop iteration if older than one week
            }
        }

        count++;

        // Get and format timestamp
        uint64_t usec;
        if (sd_journal_get_realtime_usec(journal, &usec) == 0) {
            time_t sec = (time_t)(usec / 1000000ULL);
            struct tm tm_info;
            localtime_r(&sec, &tm_info); // Thread-safe time conversion

            char time_buf[64];
            // Format example: [2025-11-28 10:00:00]
            size_t t_len = strftime(time_buf, sizeof(time_buf), "[%Y-%m-%d %H:%M:%S] ", &tm_info);
            write(fd, time_buf, t_len);
        }

        // Get MESSAGE field only
        SD_JOURNAL_FOREACH_DATA(journal, data, length)
        {
            const char *field = (const char *)data;
            // Check if this is the MESSAGE field
            if (length > 8 && memcmp(field, "MESSAGE=", 8) == 0) {
                // Write message content without "MESSAGE=" prefix
                write(fd, field + 8, length - 8);
                write(fd, "\n", 1);
                break; // Found MESSAGE, no need to continue
            }
        }
    }

    // If no entries found, write a message
    if (count == 0) {
        const char *msg = "No log entries found (Permission denied or empty?)\n";
        write(fd, msg, strlen(msg));
    }

    // Reset file position to beginning
    lseek(fd, 0, SEEK_SET);

    // Return the file descriptor
    r = sd_bus_reply_method_return(m, "h", fd);

cleanup:
    // Clean up resources
    if (journal) {
        sd_journal_close(journal);
    }
    if (fd >= 0) {
        close(fd);
    }

    return r;
}

/* ErrorOccurred signal emission */
int emit_error_occurred_signal(const struct err_notify_data *data,
                               const char *cgroup,
                               const char *cmdline)
{
    if (!g_bus) {
        log_warning("DBus not initialized");
        return -1;
    }

    if (!data) {
        log_warning("Invalid data pointer");
        return -1;
    }

    // Check if pid is valid
    if (data->pid <= 0) {
        log_warning("Invalid pid: %d", data->pid);
        return -1;
    }

    // Check if notifications are enabled
    if (!g_enabled) {
        return 0; // Silently skip
    }

    // Check rate limit
    if (!rate_limit_table_check(&g_rate_limit_table, cgroup, cmdline)) {
        log_debug("Rate limit exceeded for cgroup=%s cmdline=%s",
                  cgroup ? cgroup : "(null)",
                  cmdline ? cmdline : "(null)");
        return 0;
    }

    sd_bus_message *signal = NULL;
    int r;

    // Create a new signal message
    r = sd_bus_message_new_signal(g_bus,
                                  &signal,
                                  DBUS_OBJECT_PATH,
                                  DBUS_INTERFACE_NAME,
                                  "ErrorOccurred");
    if (r < 0) {
        log_warning("Failed to create signal: %s", strerror(-r));
        return r;
    }

    // Open a{sv} container (dictionary of string to variant)
    r = sd_bus_message_open_container(signal, 'a', "{sv}");
    if (r < 0) {
        log_warning("Failed to open container: %s", strerror(-r));
        sd_bus_message_unref(signal);
        return r;
    }

    // Add pid field
    r = sd_bus_message_open_container(signal, 'e', "sv");
    if (r < 0)
        goto finish;
    r = sd_bus_message_append(signal, "s", "pid");
    if (r < 0)
        goto finish;
    r = sd_bus_message_open_container(signal, 'v', "u");
    if (r < 0)
        goto finish;
    r = sd_bus_message_append(signal, "u", (uint32_t)data->pid);
    if (r < 0)
        goto finish;
    r = sd_bus_message_close_container(signal);
    if (r < 0)
        goto finish;
    r = sd_bus_message_close_container(signal);
    if (r < 0)
        goto finish;

    // Add cgroup field
    if (cgroup && cgroup[0]) {
        log_debug("Get cgroup path: %s", cgroup);

        r = sd_bus_message_open_container(signal, 'e', "sv");
        if (r < 0)
            goto finish;
        r = sd_bus_message_append(signal, "s", "cgroup");
        if (r < 0)
            goto finish;
        r = sd_bus_message_open_container(signal, 'v', "s");
        if (r < 0)
            goto finish;
        r = sd_bus_message_append(signal, "s", cgroup);
        if (r < 0)
            goto finish;
        r = sd_bus_message_close_container(signal);
        if (r < 0)
            goto finish;
        r = sd_bus_message_close_container(signal);
        if (r < 0)
            goto finish;
    }

    // Add cmdline field
    if (cmdline && cmdline[0]) {
        r = sd_bus_message_open_container(signal, 'e', "sv");
        if (r < 0)
            goto finish;
        r = sd_bus_message_append(signal, "s", "cmdline");
        if (r < 0)
            goto finish;
        r = sd_bus_message_open_container(signal, 'v', "s");
        if (r < 0)
            goto finish;
        r = sd_bus_message_append(signal, "s", cmdline);
        if (r < 0)
            goto finish;
        r = sd_bus_message_close_container(signal);
        if (r < 0)
            goto finish;
        r = sd_bus_message_close_container(signal);
        if (r < 0)
            goto finish;
    }

    // Close the a{sv} container
    r = sd_bus_message_close_container(signal);
    if (r < 0) {
        log_warning("Failed to close container: %s", strerror(-r));
        goto finish;
    }

    // Send the signal
    r = sd_bus_send(g_bus, signal, NULL);
    if (r < 0) {
        log_warning("Failed to send signal: %s", strerror(-r));
    } else {
        log_debug("ErrorOccurred signal sent successfully");
    }

finish:
    sd_bus_message_unref(signal);
    return r;
}

/* ========== sd-event loop implementation ========== */

/* Netlink FD readable callback */
static int netlink_io_handler(sd_event_source *s, int fd, uint32_t revents, void *userdata)
{
    (void)s;
    (void)fd;

    struct err_notify_state *state = (struct err_notify_state *)userdata;

    if (revents & EPOLLIN) {
        // Process one netlink message (non-blocking)
        int ret = err_notify_process_one(state);
        if (ret < 0 && ret != -NLE_AGAIN) {
            log_warning("Error processing netlink message: %d", ret);
        }
    }

    if (revents & (EPOLLERR | EPOLLHUP)) {
        log_warning("Netlink socket error or hangup");
        return -1;
    }

    return 0;
}

/* sd-event loop thread function */
static void *event_loop_thread(void *arg)
{
    (void)arg;

    log_info("sd-event loop thread started");

    // Run the event loop (blocks until sd_event_exit is called)
    int ret = sd_event_loop(g_event);
    if (ret < 0) {
        log_warning("Event loop failed: %s", strerror(-ret));
    }

    log_info("sd-event loop thread exiting");
    return NULL;
}

/* Initialize sd-event loop */
int event_loop_init(sd_bus *bus, struct err_notify_state *netlink_state)
{
    int ret;

    if (!bus || !netlink_state) {
        log_warning("Invalid parameters for event_loop_init");
        return -1;
    }

    // 1. Create sd_event instance
    ret = sd_event_new(&g_event);
    if (ret < 0) {
        log_warning("Failed to create event loop: %s", strerror(-ret));
        return -1;
    }

    // 2. Attach sd_bus to event loop
    ret = sd_bus_attach_event(bus, g_event, SD_EVENT_PRIORITY_NORMAL);
    if (ret < 0) {
        log_warning("Failed to attach bus to event: %s", strerror(-ret));
        sd_event_unref(g_event);
        g_event = NULL;
        return -1;
    }

    // 3. Get netlink socket fd
    int nl_fd = err_notify_get_fd(netlink_state);
    if (nl_fd < 0) {
        log_warning("Failed to get netlink fd");
        sd_event_unref(g_event);
        g_event = NULL;
        return -1;
    }

    log_info("Netlink socket fd: %d", nl_fd);

    // 4. Add netlink fd to event loop
    ret = sd_event_add_io(g_event,
                          &g_netlink_source,
                          nl_fd,
                          EPOLLIN,
                          netlink_io_handler,
                          netlink_state);
    if (ret < 0) {
        log_warning("Failed to add netlink fd to event: %s", strerror(-ret));
        sd_event_unref(g_event);
        g_event = NULL;
        return -1;
    }

    // 5. Set description for the event source (for debugging)
    ret = sd_event_source_set_description(g_netlink_source, "netlink-error-notify");
    if (ret < 0) {
        log_debug("Failed to set event source description: %s", strerror(-ret));
        // Not critical, continue
    }

    // 6. Start event loop thread
    if (pthread_create(&g_event_thread, NULL, event_loop_thread, NULL) != 0) {
        log_warning("Failed to create event loop thread");
        sd_event_source_unref(g_netlink_source);
        sd_event_unref(g_event);
        g_event = NULL;
        g_netlink_source = NULL;
        return -1;
    }

    g_event_thread_running = true;
    log_info("Event loop initialized successfully");

    return 0;
}

/* Cleanup sd-event loop */
void event_loop_cleanup(void)
{
    if (g_event_thread_running) {
        log_info("Stopping event loop thread...");

        // Request exit from event loop
        if (g_event) {
            sd_event_exit(g_event, 0);
        }

        // Wait for thread to exit
        pthread_join(g_event_thread, NULL);
        g_event_thread_running = false;

        log_info("Event loop thread stopped");
    }

    // Cleanup resources
    if (g_netlink_source) {
        sd_event_source_unref(g_netlink_source);
        g_netlink_source = NULL;
    }

    if (g_event) {
        sd_event_unref(g_event);
        g_event = NULL;
    }
}
