/************************************************************************
 * fspy - Linux filesystem activity monitor            *
 * ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ *
 * fanotify implementation for process tracking                          *
 ************************************************************************
 * Copyright (C) 2025 Bean Huo <beanhuo@micron.com>                       *
 *                                                                      *
 * This program is free software; you can redistribute it and/or modify *
 * it under the terms of the GNU General Public License as published by *
 * the Free Software Foundation; either version 2 of the License, or    *
 * (at your option) any later version.                                  *
 ************************************************************************/

#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <fcntl.h>
#include <signal.h>
#include <sys/fanotify.h>
#include <sys/stat.h>
#include <sys/inotify.h>
#include <limits.h>
#include <time.h>

#include "fspy.h"
#include "fanotify_events.h"
#include "output.h"
#include "stating.h"
#include "diff.h"
#include "enumdirs.h"
#include "regmatch.h"

extern volatile sig_atomic_t sigint;
extern char *typestring;
extern char *filterstring;
extern char *ifilterstring;
extern char *diffstring;
extern struct diffprint dprint;
extern unsigned int delc_oa; /* from diff.c */
extern struct festat *felsptr; /* from diff.c */

/* Convert fanotify event mask to inotify-like event for compatibility */
static struct inotify_event* fanotify_to_inotify_event(const struct fanotify_event_metadata *metadata,
                                                         const char *filename, pid_t pid)
{
	/* Static buffer for event + name */
	static char event_buf[sizeof(struct inotify_event) + 256];
	struct inotify_event *event = (struct inotify_event *)event_buf;

	memset(event_buf, 0, sizeof(event_buf));

	/* Store PID in wd field (we'll use this in output.c) */
	event->wd = pid;

	/* Map fanotify masks to inotify masks */
	if (metadata->mask & FAN_OPEN)
		event->mask = IN_OPEN;
	else if (metadata->mask & FAN_CLOSE_WRITE)
		event->mask = IN_CLOSE_WRITE;
	else if (metadata->mask & FAN_CLOSE_NOWRITE)
		event->mask = IN_CLOSE_NOWRITE;
	else if (metadata->mask & FAN_MODIFY)
		event->mask = IN_MODIFY;
	else if (metadata->mask & FAN_ACCESS)
		event->mask = IN_ACCESS;

	/* Copy filename to event->name */
	if (filename) {
		size_t nlen = strlen(filename);

		if (nlen > 255)
			nlen = 255;
		memcpy(event->name, filename, nlen);
		event->name[nlen] = '\0';
		event->len = nlen + 1;
	}

	return event;
}

/* Get filename from fd */
static int get_filename_from_fd(int fd, char *buf, size_t size)
{
	char proc_path[64];
	ssize_t len;

	snprintf(proc_path, sizeof(proc_path), "/proc/self/fd/%d", fd);
	len = readlink(proc_path, buf, size - 1);
	if (len < 0)
		return -1;

	buf[len] = '\0';
	return 0;
}

/* Extract directory and filename from full path */
static void split_path(const char *fullpath, char *dir, char *filename, size_t dir_size, size_t name_size)
{
	const char *last_slash = strrchr(fullpath, '/');

	if (last_slash) {
		size_t dir_len = last_slash - fullpath + 1;
		size_t name_len = strlen(last_slash + 1);

		if (dir_len >= dir_size)
			dir_len = dir_size - 1;
		memcpy(dir, fullpath, dir_len);
		dir[dir_len] = '\0';

		if (name_len >= name_size)
			name_len = name_size - 1;
		memcpy(filename, last_slash + 1, name_len);
		filename[name_len] = '\0';
	} else {
		dir[0] = '/';
		dir[1] = '\0';

		size_t name_len = strlen(fullpath);

		if (name_len >= name_size)
			name_len = name_size - 1;
		memcpy(filename, fullpath, name_len);
		filename[name_len] = '\0';
	}
}

int fanotify_monitor(const char *path, char *output_fmt, char *filter,
                     char *ifilter, char *types, char *diff)
{
	int fan_fd;
	char buf[4096];
	ssize_t len;
	char fullpath[PATH_MAX];
	char dir[PATH_MAX];
	char filename[256];
	struct stat statdat;
	struct inotify_event *event;
	char *tmp_output_string = NULL;

	/* Initialize fanotify */
	fan_fd = fanotify_init(FAN_CLASS_NOTIF | FAN_UNLIMITED_QUEUE | FAN_UNLIMITED_MARKS, O_RDONLY);
	if (fan_fd < 0) {
		perror("fanotify_init");
		fprintf(stderr, "ERROR: Failed to initialize fanotify. Are you running as root?\n");
		return -1;
	}

	/* Mark the mount point for monitoring */
	if (fanotify_mark(fan_fd, FAN_MARK_ADD | FAN_MARK_MOUNT,
	                  FAN_OPEN | FAN_CLOSE | FAN_MODIFY | FAN_ACCESS,
	                  AT_FDCWD, path) < 0) {
		perror("fanotify_mark");
		fprintf(stderr, "ERROR: Failed to mark path for monitoring: %s\n", path);
		close(fan_fd);
		return -1;
	}

	fprintf(stderr, "Monitoring %s with fanotify (process tracking enabled)\n", path);

	/* Initialize diff tracking if enabled */
	if (diff)
		felsptr = init_diff();

	/* Setup output format - use passed parameter, fallback to env var, then default */
	if (output_fmt) {
		tmp_output_string = output_fmt;
	} else {
		tmp_output_string = getenv("FSPY_OUTPUT");
		if (!tmp_output_string)
			tmp_output_string = "[,T,] ,d,:,p,f";
	}

	/* Event loop */
	while (sigint) {
		len = read(fan_fd, buf, sizeof(buf));
		if (len < 0) {
			if (sigint == 0)
				break;
			/* EINTR is expected when SIGINT arrives during read */
			if (errno == EINTR)
				continue;
			perror("read");
			continue;
		}

		struct fanotify_event_metadata *metadata;
		metadata = (struct fanotify_event_metadata *)buf;

		while (sigint && FAN_EVENT_OK(metadata, len)) {
			if (metadata->vers != FANOTIFY_METADATA_VERSION) {
				fprintf(stderr, "Mismatch of fanotify metadata version\n");
				break;
			}

			/* Skip if not a file event */
			if (metadata->fd < 0)
				goto next_event;

			/* Get the filename */
			if (get_filename_from_fd(metadata->fd, fullpath, sizeof(fullpath)) < 0)
				goto close_fd;

			/* Get file stats */
			if (stat(fullpath, &statdat) < 0)
				goto close_fd;

			/* Apply regex filter if specified */
			if (filter && !reg_match(fullpath))
				goto close_fd;

			/* Apply inverted regex filter if specified */
			if (ifilter && !ireg_match(fullpath))
				goto close_fd;

			/* Apply type filter if specified */
			if (types && !checktype(fullpath, NULL, types, &statdat))
				goto close_fd;

			/* Handle diffing if enabled */
			if (diff) {
				unsigned int di;
				int found = 0;

				/* Search for this file in the diff element list */
				for (di = 0; di < delc_oa; di++) {
					if (strcmp(felsptr[di].path, fullpath) == 0) {
						found = 1;
						break;
					}
				}

				/* Register new file in the diff list if not found */
				if (!found) {
					if (delc_oa > 0 && (delc_oa % DIFF_ELEMENT_INIT_COUNT) == 0)
						felsptr = extend_diff_list(felsptr);
					memcpy(felsptr[delc_oa].path, fullpath, strlen(fullpath) + 1);
					memcpy(&felsptr[delc_oa].statdat, &statdat, sizeof(struct stat));
					felsptr[delc_oa].id = delc_oa;
					delc_oa++;
				}

				dprint.s = dprint.A = dprint.M = dprint.S = dprint.O = dprint.U = dprint.G =
				           dprint.I = dprint.D = 0;
				diffing(fullpath, &statdat, &dprint, diff);
			}

			/* Split path into directory and filename */
			split_path(fullpath, dir, filename, sizeof(dir), sizeof(filename));

			/* Convert to inotify-like event and print */
			event = fanotify_to_inotify_event(metadata, filename, metadata->pid);
			print_data(tmp_output_string, event, dir, &statdat, &dprint);

close_fd:
			close(metadata->fd);

next_event:
			metadata = FAN_EVENT_NEXT(metadata, len);
		}
	}

	close(fan_fd);
	return 0;
}
