#include "git-compat-util.h"
#include "fsmonitor-ll.h"
#include "fsmonitor-path-utils.h"
#include "gettext.h"
#include "trace.h"

#include <sys/statfs.h>

#ifdef HAVE_LINUX_MAGIC_H
#include <linux/magic.h>
#endif

/*
 * Filesystem magic numbers for remote filesystems.
 * Defined here if not available in linux/magic.h.
 */
#ifndef CIFS_SUPER_MAGIC
#define CIFS_SUPER_MAGIC 0xff534d42
#endif
#ifndef SMB_SUPER_MAGIC
#define SMB_SUPER_MAGIC 0x517b
#endif
#ifndef SMB2_SUPER_MAGIC
#define SMB2_SUPER_MAGIC 0xfe534d42
#endif
#ifndef NFS_SUPER_MAGIC
#define NFS_SUPER_MAGIC 0x6969
#endif
#ifndef AFS_SUPER_MAGIC
#define AFS_SUPER_MAGIC 0x5346414f
#endif
#ifndef CODA_SUPER_MAGIC
#define CODA_SUPER_MAGIC 0x73757245
#endif
#ifndef FUSE_SUPER_MAGIC
#define FUSE_SUPER_MAGIC 0x65735546
#endif

/*
 * Check if filesystem type is a remote filesystem.
 */
static int is_remote_fs(unsigned long f_type)
{
	switch (f_type) {
	case CIFS_SUPER_MAGIC:
	case SMB_SUPER_MAGIC:
	case SMB2_SUPER_MAGIC:
	case NFS_SUPER_MAGIC:
	case AFS_SUPER_MAGIC:
	case CODA_SUPER_MAGIC:
	case FUSE_SUPER_MAGIC:
		return 1;
	default:
		return 0;
	}
}

/*
 * Map filesystem magic numbers to human-readable names as a fallback
 * when /proc/mounts is unavailable.  This only covers the remote and
 * special filesystems in is_remote_fs() above; local filesystems are
 * never flagged as incompatible, so we do not need their names here.
 */
static const char *get_fs_typename(unsigned long f_type)
{
	switch (f_type) {
	case CIFS_SUPER_MAGIC:
		return "cifs";
	case SMB_SUPER_MAGIC:
		return "smb";
	case SMB2_SUPER_MAGIC:
		return "smb2";
	case NFS_SUPER_MAGIC:
		return "nfs";
	case AFS_SUPER_MAGIC:
		return "afs";
	case CODA_SUPER_MAGIC:
		return "coda";
	case FUSE_SUPER_MAGIC:
		return "fuse";
	default:
		return "unknown";
	}
}

/*
 * Find the mount point for a given path by reading /proc/mounts.
 *
 * statfs(2) gives us f_type (the magic number) but not the human-readable
 * filesystem type string.  We scan /proc/mounts to find the mount entry
 * whose path is the longest prefix of ours and whose f_fsid matches,
 * which gives us the fstype string (e.g. "nfs", "ext4") for logging.
 */
static char *find_mount(const char *path, const struct statfs *path_fs)
{
	FILE *fp;
	struct strbuf line = STRBUF_INIT;
	struct strbuf match = STRBUF_INIT;
	struct strbuf fstype = STRBUF_INIT;
	char *result = NULL;

	fp = fopen("/proc/mounts", "r");
	if (!fp)
		return NULL;

	while (strbuf_getline(&line, fp) != EOF) {
		char *fields[6];
		char *p = line.buf;
		int i;

		/* Parse mount entry: device mountpoint fstype options dump pass */
		for (i = 0; i < 6 && p; i++) {
			fields[i] = p;
			p = strchr(p, ' ');
			if (p)
				*p++ = '\0';
		}

		if (i >= 3) {
			const char *mountpoint = fields[1];
			const char *type = fields[2];
			struct statfs mount_fs;

			/* Check if this mount point is a prefix of our path */
			if (starts_with(path, mountpoint) &&
			    (path[strlen(mountpoint)] == '/' ||
			     path[strlen(mountpoint)] == '\0')) {
				/* Check if filesystem ID matches */
				if (statfs(mountpoint, &mount_fs) == 0 &&
				    !memcmp(&mount_fs.f_fsid, &path_fs->f_fsid,
					    sizeof(mount_fs.f_fsid))) {
					/* Keep the longest matching mount point */
					if (strlen(mountpoint) > match.len) {
						strbuf_reset(&match);
						strbuf_addstr(&match, mountpoint);
						strbuf_reset(&fstype);
						strbuf_addstr(&fstype, type);
					}
				}
			}
		}
	}

	fclose(fp);
	strbuf_release(&line);
	strbuf_release(&match);

	if (fstype.len)
		result = strbuf_detach(&fstype, NULL);
	else
		strbuf_release(&fstype);

	return result;
}

int fsmonitor__get_fs_info(const char *path, struct fs_info *fs_info)
{
	struct statfs fs;

	if (statfs(path, &fs) == -1) {
		int saved_errno = errno;
		trace_printf_key(&trace_fsmonitor, "statfs('%s') failed: %s",
				 path, strerror(saved_errno));
		errno = saved_errno;
		return -1;
	}

	trace_printf_key(&trace_fsmonitor,
			 "statfs('%s') [type 0x%08lx]",
			 path, (unsigned long)fs.f_type);

	fs_info->is_remote = is_remote_fs(fs.f_type);

	/*
	 * Try to get filesystem type from /proc/mounts for a more
	 * descriptive name.
	 */
	fs_info->typename = find_mount(path, &fs);
	if (!fs_info->typename)
		fs_info->typename = xstrdup(get_fs_typename(fs.f_type));

	trace_printf_key(&trace_fsmonitor,
			 "'%s' is_remote: %d, typename: %s",
			 path, fs_info->is_remote, fs_info->typename);

	return 0;
}

int fsmonitor__is_fs_remote(const char *path)
{
	struct fs_info fs;

	if (fsmonitor__get_fs_info(path, &fs))
		return -1;

	free(fs.typename);

	return fs.is_remote;
}

/*
 * No-op for Linux - we don't have firmlinks like macOS.
 */
int fsmonitor__get_alias(const char *path UNUSED,
			 struct alias_info *info UNUSED)
{
	return 0;
}

/*
 * No-op for Linux - we don't have firmlinks like macOS.
 */
char *fsmonitor__resolve_alias(const char *path UNUSED,
			       const struct alias_info *info UNUSED)
{
	return NULL;
}
