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

#define _GNU_SOURCE

#include "netlink_client.h"

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

#include <errno.h>
#include <libgen.h>
#include <limits.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>

#define MAX_ATTR_ARRAY_SIZE 2

/* Internal state structure */
struct err_notify_state
{
    struct nl_sock *sock;
    int family_id;
    err_notify_callback_t callback;
    void *callback_userdata;
};

/* Global options */
static bool opt_verbose = false;

/* Global variable for signal handling */
static volatile sig_atomic_t keep_running = 1;

void err_notify_set_verbose(bool verbose)
{
    opt_verbose = verbose;
}

void err_notify_set_callback(struct err_notify_state *state,
                             err_notify_callback_t callback,
                             void *userdata)
{
    if (state) {
        state->callback = callback;
        state->callback_userdata = userdata;
    }
}

/**
 * validate_inodes - Verify that inodes match actual files on disk
 *
 * Validation strategy: "First match succeeds" (design feature)
 * - For multi-file notifications, only the first successfully validated
 *   filename/inode pair is checked; remaining pairs are NOT validated
 * - This is intentional: once any file/inode validates, the notification
 *   is considered legitimate and processing continues
 * - Rationale: In many cases, kernel reports multiple paths to the same
 *   operation (e.g., rename), validating one is sufficient
 */
static bool validate_inodes(char **filenames,
                            int filename_count,
                            ino_t *inodes,
                            int inode_count,
                            bool *inode_parents,
                            int inode_parent_count)
{
    int i;
    struct stat st;
    char path_copy[PATH_MAX];
    char *parent_dir;

    if (inode_count == 0) {
        fprintf(stderr, "Error: No inodes provided, failed validation\n");
        return false;
    }

    for (i = 0; i < filename_count; i++) {
        ino_t inode = inodes[i];
        bool is_parent_inode = inode_parents[i];

        if (inode == 0) {
            if (opt_verbose) {
                printf("Warning: Inode is 0, skipping validation\n");
            }
            continue;
        }

        if (is_parent_inode) {
            strncpy(path_copy, filenames[i], sizeof(path_copy) - 1);
            path_copy[sizeof(path_copy) - 1] = '\0';
            parent_dir = dirname(path_copy);

            if (lstat(parent_dir, &st) != 0) {
                if (opt_verbose) {
                    fprintf(stderr,
                            "Warning: Cannot stat parent directory %s: %s (trying next)\n",
                            parent_dir,
                            strerror(errno));
                }
                continue;
            }

            if (st.st_ino != inode) {
                fprintf(stderr,
                        "Error: Parent inode mismatch for file %s: expected %lu, got %lu\n",
                        filenames[i],
                        inode,
                        (unsigned long)st.st_ino);
                return false;
            }

            if (opt_verbose) {
                printf("Info: Validated parent inode for %s (parent: %s, inode: %lu)\n",
                       filenames[i],
                       parent_dir,
                       inode);
            }
            // First match succeeds - remaining files are NOT validated (by design)
            return true;
        }

        if (lstat(filenames[i], &st) != 0) {
            if (opt_verbose) {
                fprintf(stderr,
                        "Warning: Cannot stat file %s: %s (trying next)\n",
                        filenames[i],
                        strerror(errno));
            }
            continue;
        }

        if (st.st_ino != inode) {
            fprintf(stderr,
                    "Error: Inode mismatch for file %s: expected %lu, got %lu\n",
                    filenames[i],
                    inode,
                    (unsigned long)st.st_ino);
            return false;
        }

        if (opt_verbose) {
            printf("Info: Validated inode for %s (inode: %lu)\n", filenames[i], inode);
        }
        // First match succeeds - remaining files are NOT validated (by design)
        return true;
    }

    fprintf(stderr, "Error: All %d inode(s) failed validation\n", inode_count);
    return false;
}

/* Callback function to handle multicast notifications */
static int notification_handler(struct nl_msg *msg, void *arg)
{
    struct err_notify_state *state = (struct err_notify_state *)arg;
    struct nlmsghdr *nlh = nlmsg_hdr(msg);
    struct genlmsghdr *gnlh = genlmsg_hdr(nlh);
    struct nlattr *attrs[__DEEPIN_ERR_NOTIFY_ATTR_MAX];
    struct nlattr *attr;
    int ret;
    int remaining;
    char *filenames[MAX_ATTR_ARRAY_SIZE];
    int filename_count = 0;
    ino_t inodes[MAX_ATTR_ARRAY_SIZE];
    int inode_count = 0;
    bool inode_parents[MAX_ATTR_ARRAY_SIZE];
    int inode_parent_count = 0;
    bool truncated = false;

    // Check if state and callback are set before parsing
    if (!state || !state->callback) {
        fprintf(stderr, "Error: Notification handler called with null state or callback\n");
        return NL_SKIP;
    }

    ret = genlmsg_parse(nlh, 0, attrs, DEEPIN_ERR_NOTIFY_ATTR_MAX, NULL);
    if (ret < 0) {
        fprintf(stderr, "failed to parse notification attributes: %s\n", nl_geterror(ret));
        return NL_SKIP;
    }

    // Check array length consistency
    if (filename_count != inode_count || filename_count != inode_parent_count) {
        fprintf(stderr,
                "Error: Invalid message - array length mismatch "
                "(filenames=%d, inodes=%d, inode_parents=%d)\n",
                filename_count,
                inode_count,
                inode_parent_count);
        return NL_SKIP;
    }

    nla_for_each_attr(attr, genlmsg_attrdata(gnlh, 0), genlmsg_attrlen(gnlh, 0), remaining)
    {
        if (nla_type(attr) == DEEPIN_ERR_NOTIFY_ATTR_FILENAME) {
            if (filename_count < MAX_ATTR_ARRAY_SIZE) {
                filenames[filename_count++] = nla_get_string(attr);
            } else {
                truncated = true;
            }
        } else if (nla_type(attr) == DEEPIN_ERR_NOTIFY_ATTR_INODE) {
            if (inode_count < MAX_ATTR_ARRAY_SIZE) {
                inodes[inode_count++] = (ino_t)nla_get_u64(attr);
            } else {
                truncated = true;
            }
        } else if (nla_type(attr) == DEEPIN_ERR_NOTIFY_ATTR_INODE_PARENT) {
            if (inode_parent_count < MAX_ATTR_ARRAY_SIZE) {
                inode_parents[inode_parent_count++] = (bool)nla_get_u8(attr);
            } else {
                truncated = true;
            }
        }
    }

    // Warn once if any attributes were truncated
    if (truncated) {
        fprintf(stderr,
                "Warning: Attribute count exceeds limit (%d), some attributes (filenames, inode_count, inode_parent_count) truncated\n",
                MAX_ATTR_ARRAY_SIZE);
    }

    // Check if inode validation is successful
    if (!validate_inodes(filenames,
                         filename_count,
                         inodes,
                         inode_count,
                         inode_parents,
                         inode_parent_count)) {
        fprintf(stderr, "Notification rejected: inode validation failed\n");
        return NL_SKIP;
    }

    // Prepare notification data for callback
    struct err_notify_data data = { 0 };

    // Set inode arrays and count
    if (inode_count > 0) {
        data.inode_count = inode_count;
        data.inodes = inodes;
        data.inode_parents = inode_parents;
        data.filenames = (const char **)filenames;
    }

    // Set process information
    if (attrs[DEEPIN_ERR_NOTIFY_ATTR_PID]) {
        data.pid = (pid_t)nla_get_u32(attrs[DEEPIN_ERR_NOTIFY_ATTR_PID]);
    }
    if (attrs[DEEPIN_ERR_NOTIFY_ATTR_PPID]) {
        data.ppid = (pid_t)nla_get_u32(attrs[DEEPIN_ERR_NOTIFY_ATTR_PPID]);
    }
    if (attrs[DEEPIN_ERR_NOTIFY_ATTR_COMM]) {
        data.comm = nla_get_string(attrs[DEEPIN_ERR_NOTIFY_ATTR_COMM]);
    }
    if (attrs[DEEPIN_ERR_NOTIFY_ATTR_UID]) {
        data.uid = (uid_t)nla_get_u32(attrs[DEEPIN_ERR_NOTIFY_ATTR_UID]);
    }
    if (attrs[DEEPIN_ERR_NOTIFY_ATTR_GID]) {
        data.gid = (gid_t)nla_get_u32(attrs[DEEPIN_ERR_NOTIFY_ATTR_GID]);
    }

    state->callback(&data, state->callback_userdata);

    return NL_OK;
}

/* Sequence check callback */
static int seq_check_handler(struct nl_msg *msg __attribute__((unused)),
                             void *arg __attribute__((unused)))
{
    return NL_OK;
}

/* Error callback function */
static int error_handler(struct sockaddr_nl *nla __attribute__((unused)),
                         struct nlmsgerr *err,
                         void *arg __attribute__((unused)))
{
    fprintf(stderr, "netlink error: %s\n", strerror(-err->error));
    return NL_STOP;
}

int err_notify_init(struct err_notify_state **state_ptr)
{
    int ret;
    struct err_notify_state *state;

    if (!state_ptr) {
        return -1;
    }

    state = calloc(1, sizeof(struct err_notify_state));
    if (!state) {
        fprintf(stderr, "failed to allocate state structure\n");
        return -1;
    }

    state->sock = nl_socket_alloc();
    if (!state->sock) {
        fprintf(stderr, "failed to allocate netlink socket\n");
        free(state);
        return -1;
    }

    ret = genl_connect(state->sock);
    if (ret < 0) {
        fprintf(stderr, "failed to connect to Generic Netlink: %s\n", nl_geterror(ret));
        nl_socket_free(state->sock);
        free(state);
        return -1;
    }

    state->family_id = genl_ctrl_resolve(state->sock, DEEPIN_ERR_NOTIFY_FAMILY_NAME);
    if (state->family_id < 0) {
        fprintf(stderr,
                "failed to resolve family '%s': %s\n",
                DEEPIN_ERR_NOTIFY_FAMILY_NAME,
                nl_geterror(state->family_id));
        fprintf(stderr, "make sure the kernel module is loaded\n");
        nl_socket_free(state->sock);
        free(state);
        return -1;
    }

    printf("Found family '%s' with ID: %d\n", DEEPIN_ERR_NOTIFY_FAMILY_NAME, state->family_id);

    *state_ptr = state;
    return 0;
}

void err_notify_cleanup(struct err_notify_state *state)
{
    if (state) {
        if (state->sock) {
            nl_socket_free(state->sock);
        }
        free(state);
    }
}

int err_notify_enable(struct err_notify_state *state)
{
    struct nl_msg *msg;
    void *hdr;
    int ret;

    if (!state) {
        return -1;
    }

    msg = nlmsg_alloc();
    if (!msg) {
        fprintf(stderr, "failed to allocate netlink message\n");
        return -1;
    }

    hdr = genlmsg_put(msg,
                      NL_AUTO_PORT,
                      NL_AUTO_SEQ,
                      state->family_id,
                      0,
                      NLM_F_REQUEST | NLM_F_ACK,
                      DEEPIN_ERR_NOTIFY_CMD_ENABLE,
                      1);
    if (!hdr) {
        fprintf(stderr, "failed to add Generic Netlink header\n");
        nlmsg_free(msg);
        return -1;
    }

    ret = nl_send_auto(state->sock, msg);
    nlmsg_free(msg);
    if (ret < 0) {
        fprintf(stderr, "failed to send enable command: %s\n", nl_geterror(ret));
        return -1;
    }

    ret = nl_wait_for_ack(state->sock);
    if (ret < 0) {
        fprintf(stderr,
                "failed to receive ACK for enable command: %s, err code: %d\n",
                nl_geterror(ret),
                ret);
        if (ret == -NLE_PERM)
            fprintf(stderr, "insufficient permissions (need CAP_NET_ADMIN or root)\n");
        return -1;
    }

    printf("Error notification enabled successfully\n");
    return 0;
}

int err_notify_disable(struct err_notify_state *state)
{
    struct nl_msg *msg;
    void *hdr;
    int ret;

    if (!state) {
        return -1;
    }

    msg = nlmsg_alloc();
    if (!msg) {
        fprintf(stderr, "failed to allocate netlink message\n");
        return -1;
    }

    hdr = genlmsg_put(msg,
                      NL_AUTO_PORT,
                      NL_AUTO_SEQ,
                      state->family_id,
                      0,
                      NLM_F_REQUEST | NLM_F_ACK,
                      DEEPIN_ERR_NOTIFY_CMD_DISABLE,
                      1);
    if (!hdr) {
        fprintf(stderr, "failed to add Generic Netlink header\n");
        nlmsg_free(msg);
        return -1;
    }

    ret = nl_send_auto(state->sock, msg);
    nlmsg_free(msg);
    if (ret < 0) {
        fprintf(stderr, "failed to send disable command: %s\n", nl_geterror(ret));
        return -1;
    }

    ret = nl_wait_for_ack(state->sock);
    if (ret < 0) {
        fprintf(stderr,
                "failed to receive ACK for disable command: %s, err code: %d\n",
                nl_geterror(ret),
                ret);
        if (ret == -NLE_PERM)
            fprintf(stderr, "insufficient permissions (need CAP_NET_ADMIN or root)\n");
        return -1;
    }

    printf("Error notification disabled successfully\n");
    return 0;
}

/* Callback function for handling get_status response */
static int get_status_callback(struct nl_msg *msg, void *arg)
{
    bool *status = (bool *)arg;
    struct nlmsghdr *nlh = nlmsg_hdr(msg);
    struct nlattr *attrs[__DEEPIN_ERR_NOTIFY_ATTR_MAX];
    int ret;

    ret = genlmsg_parse(nlh, 0, attrs, DEEPIN_ERR_NOTIFY_ATTR_MAX, NULL);
    if (ret < 0) {
        fprintf(stderr, "failed to parse status response: %s\n", nl_geterror(ret));
        return NL_SKIP;
    }

    if (!attrs[DEEPIN_ERR_NOTIFY_ATTR_STATUS]) {
        fprintf(stderr, "status attribute not found in response\n");
        return NL_SKIP;
    }

    *status = nla_get_u8(attrs[DEEPIN_ERR_NOTIFY_ATTR_STATUS]) != 0;
    return NL_OK;
}

int err_notify_get_status(struct err_notify_state *state)
{
    struct nl_msg *msg;
    struct nl_cb *cb;
    void *hdr;
    int ret;
    bool status = false;

    if (!state) {
        return -1;
    }

    msg = nlmsg_alloc();
    if (!msg) {
        fprintf(stderr, "failed to allocate netlink message\n");
        return -1;
    }

    hdr = genlmsg_put(msg,
                      NL_AUTO_PORT,
                      NL_AUTO_SEQ,
                      state->family_id,
                      0,
                      NLM_F_REQUEST,
                      DEEPIN_ERR_NOTIFY_CMD_GET_STATUS,
                      1);
    if (!hdr) {
        fprintf(stderr, "failed to add Generic Netlink header\n");
        nlmsg_free(msg);
        return -1;
    }

    cb = nl_cb_alloc(NL_CB_DEFAULT);
    if (!cb) {
        fprintf(stderr, "failed to allocate callback structure\n");
        nlmsg_free(msg);
        return -1;
    }

    nl_cb_set(cb, NL_CB_VALID, NL_CB_CUSTOM, get_status_callback, &status);
    nl_socket_set_cb(state->sock, cb);

    ret = nl_send_auto(state->sock, msg);
    nlmsg_free(msg);
    if (ret < 0) {
        fprintf(stderr, "failed to send get_status command: %s\n", nl_geterror(ret));
        nl_cb_put(cb);
        return -1;
    }

    ret = nl_recvmsgs_default(state->sock);
    nl_cb_put(cb);
    if (ret < 0) {
        fprintf(stderr,
                "failed to receive response for get_status command: %s, err code: %d\n",
                nl_geterror(ret),
                ret);
        return -1;
    }

    printf("Error notification is currently: %s\n", status ? "ENABLED" : "DISABLED");
    return 0;
}

/* Internal helper: join multicast group and setup callbacks
 * Returns: multicast group ID on success, negative value on failure
 */
static int join_multicast_internal(struct err_notify_state *state)
{
    int ret;
    int mcgrp_id;
    struct nl_cb *cb;

    if (!state) {
        return -1;
    }

    mcgrp_id = genl_ctrl_resolve_grp(state->sock,
                                     DEEPIN_ERR_NOTIFY_FAMILY_NAME,
                                     MULTICAST_GROUP_ERR_EVENTS);
    if (mcgrp_id < 0) {
        fprintf(stderr, "failed to resolve multicast group '%s': %s\n",
                MULTICAST_GROUP_ERR_EVENTS, nl_geterror(mcgrp_id));
        return -1;
    }

    ret = nl_socket_add_membership(state->sock, mcgrp_id);
    if (ret < 0) {
        fprintf(stderr, "failed to join multicast group: %s\n", nl_geterror(ret));
        return -1;
    }

    cb = nl_cb_alloc(NL_CB_DEFAULT);
    if (!cb) {
        fprintf(stderr, "failed to allocate callback structure\n");
        nl_socket_drop_membership(state->sock, mcgrp_id);
        return -1;
    }

    nl_cb_set(cb, NL_CB_VALID, NL_CB_CUSTOM, notification_handler, state);
    nl_cb_set(cb, NL_CB_SEQ_CHECK, NL_CB_CUSTOM, seq_check_handler, NULL);
    nl_cb_err(cb, NL_CB_CUSTOM, error_handler, NULL);
    nl_socket_set_cb(state->sock, cb);
    nl_cb_put(cb);

    return mcgrp_id;
}

int err_notify_process_one(struct err_notify_state *state)
{
    if (!state || !state->sock) {
        return -1;
    }

    /* Process one message (non-blocking) */
    int ret = nl_recvmsgs_default(state->sock);

    return ret;
}

int err_notify_get_fd(struct err_notify_state *state)
{
    if (!state || !state->sock) {
        return -1;
    }

    return nl_socket_get_fd(state->sock);
}

int err_notify_join_multicast(struct err_notify_state *state)
{
    int mcgrp_id = join_multicast_internal(state);
    if (mcgrp_id < 0) {
        return -1;
    }

    /* Set socket to non-blocking mode for event-driven processing */
    nl_socket_set_nonblocking(state->sock);

    if (opt_verbose) {
        printf("Joined multicast group successfully (non-blocking mode)\n");
    }

    return 0;
}

int err_notify_monitor(struct err_notify_state *state)
{
    int ret;

    if (!state) {
        return -1;
    }

    /* Join multicast group and setup callbacks */
    int mcgrp_id = join_multicast_internal(state);
    if (mcgrp_id < 0) {
        return -1;
    }

    /* Keep socket in blocking mode for monitor loop */
    keep_running = 1;
    while (keep_running) {
        ret = nl_recvmsgs_default(state->sock);
        if (ret < 0 && ret != -NLE_AGAIN) {
            fprintf(stderr, "error receiving messages: %s\n", nl_geterror(ret));
            break;
        }
    }

    /* Cleanup: drop multicast membership */
    nl_socket_drop_membership(state->sock, mcgrp_id);

    return 0;
}
