package main

/*
#cgo pkg-config: --static fuse3

#include "fuse-operations.h"
#include <stdlib.h>
#include <string.h>

extern void *sharefs_init(struct fuse_conn_info *conn, struct fuse_config *cfg);
extern void sharefs_destroy(void *private_data);

typedef void (*closure)();
static inline struct fuse_opt _FUSE_OPT_END() { return (struct fuse_opt)FUSE_OPT_END; };
*/
import "C"
import (
	"context"
	"encoding/gob"
	"errors"
	"fmt"
	"log"
	"os"
	"os/exec"
	"os/signal"
	"path/filepath"
	"runtime"
	"runtime/cgo"
	"strconv"
	"syscall"
	"unsafe"

	"hakurei.app/check"
	"hakurei.app/container"
	"hakurei.app/container/std"
	"hakurei.app/fhs"
	"hakurei.app/hst"
	"hakurei.app/internal/helper/proc"
	"hakurei.app/internal/info"
	"hakurei.app/message"
)

type (
	// closure represents a C function pointer.
	closure = C.closure

	// fuseArgs represents the fuse_args structure.
	fuseArgs = C.struct_fuse_args

	// setupState holds state used for setup. Its cgo handle is included in
	// sharefs_private and considered opaque to non-setup callbacks.
	setupState struct {
		// Whether sharefs_init failed.
		initFailed bool

		// Whether to create source directory as root.
		mkdir bool

		// Open file descriptor to fuse.
		Fuse int

		// Pathname to open for dirfd.
		Source *check.Absolute
		// New uid and gid to set by sharefs_init when starting as root.
		Setuid, Setgid int
	}
)

func init() { gob.Register(new(setupState)) }

// destroySetup invalidates the setup [cgo.Handle] in a sharefs_private structure.
func destroySetup(private_data unsafe.Pointer) (ok bool) {
	if private_data == nil {
		return false
	}
	priv := (*C.struct_sharefs_private)(private_data)

	if h := cgo.Handle(priv.setup); h != 0 {
		priv.setup = 0
		h.Delete()
		ok = true
	}
	return
}

//export sharefs_init
func sharefs_init(
	_ *C.struct_fuse_conn_info,
	cfg *C.struct_fuse_config,
) unsafe.Pointer {
	ctx := C.fuse_get_context()
	priv := (*C.struct_sharefs_private)(ctx.private_data)
	setup := cgo.Handle(priv.setup).Value().(*setupState)

	if os.Geteuid() == 0 {
		log.Println("filesystem daemon must not run as root")
		goto fail
	}

	cfg.use_ino = C.true
	cfg.direct_io = C.false
	// getattr is context-dependent
	cfg.attr_timeout = 0
	cfg.entry_timeout = 0
	cfg.negative_timeout = 0

	// all future filesystem operations happen through this dirfd
	if fd, err := syscall.Open(
		setup.Source.String(),
		syscall.O_DIRECTORY|syscall.O_RDONLY|syscall.O_CLOEXEC,
		0,
	); err != nil {
		log.Printf("cannot open %q: %v", setup.Source, err)
		goto fail
	} else if err = syscall.Fchdir(fd); err != nil {
		_ = syscall.Close(fd)
		log.Printf("cannot enter %q: %s", setup.Source, err)
		goto fail
	} else {
		priv.dirfd = C.int(fd)
	}

	return ctx.private_data

fail:
	setup.initFailed = true
	C.fuse_exit(ctx.fuse)
	return nil
}

//export sharefs_destroy
func sharefs_destroy(private_data unsafe.Pointer) {
	if private_data != nil {
		destroySetup(private_data)
		priv := (*C.struct_sharefs_private)(private_data)

		if err := syscall.Close(int(priv.dirfd)); err != nil {
			log.Printf("cannot close source directory: %v", err)
		}
	}
}

// showHelp prints the help message.
func showHelp(args *fuseArgs) {
	executableName := sharefsName
	if args.argc > 0 {
		executableName = filepath.Base(C.GoString(*args.argv))
	} else if name, err := os.Executable(); err == nil {
		executableName = filepath.Base(name)
	}

	fmt.Printf("usage: %s [options] <mountpoint>\n\n", executableName)

	fmt.Println("Filesystem options:")
	fmt.Println("    -o source=/data/media  source directory to be mounted")
	fmt.Println("    -o setuid=1023         uid to run as when starting as root")
	fmt.Println("    -o setgid=1023         gid to run as when starting as root")

	fmt.Println("\nFUSE options:")
	C.fuse_cmdline_help()
	C.fuse_lib_help(args)
}

// parseOpts parses fuse options via fuse_opt_parse.
func parseOpts(args *fuseArgs, setup *setupState, log *log.Logger) (ok bool) {
	var unsafeOpts struct {
		// Pathname to writable source directory.
		source *C.char

		// Whether to create source directory as root.
		mkdir C.int

		// Decimal string representation of uid to set when running as root.
		setuid *C.char
		// Decimal string representation of gid to set when running as root.
		setgid *C.char

		// Decimal string representation of open file descriptor to read
		// setupState from.
		//
		// This is an internal detail for containerisation and must not be
		// specified directly.
		setup *C.char
	}

	if C.fuse_opt_parse(args, unsafe.Pointer(&unsafeOpts), &[]C.struct_fuse_opt{
		{templ: C.CString("source=%s"), offset: C.ulong(unsafe.Offsetof(unsafeOpts.source)), value: 0},
		{templ: C.CString("mkdir"), offset: C.ulong(unsafe.Offsetof(unsafeOpts.mkdir)), value: 1},
		{templ: C.CString("setuid=%s"), offset: C.ulong(unsafe.Offsetof(unsafeOpts.setuid)), value: 0},
		{templ: C.CString("setgid=%s"), offset: C.ulong(unsafe.Offsetof(unsafeOpts.setgid)), value: 0},

		{templ: C.CString("setup=%s"), offset: C.ulong(unsafe.Offsetof(unsafeOpts.setup)), value: 0},

		C._FUSE_OPT_END(),
	}[0], nil) == -1 {
		return false
	}

	if unsafeOpts.source != nil {
		defer C.free(unsafe.Pointer(unsafeOpts.source))
	}
	if unsafeOpts.setuid != nil {
		defer C.free(unsafe.Pointer(unsafeOpts.setuid))
	}
	if unsafeOpts.setgid != nil {
		defer C.free(unsafe.Pointer(unsafeOpts.setgid))
	}

	if unsafeOpts.setup != nil {
		defer C.free(unsafe.Pointer(unsafeOpts.setup))

		if v, err := strconv.Atoi(C.GoString(unsafeOpts.setup)); err != nil || v < 3 {
			log.Println("invalid value for option setup")
			return false
		} else {
			r := os.NewFile(uintptr(v), "setup")
			defer func() {
				if err = r.Close(); err != nil {
					log.Println(err)
				}
			}()
			if err = gob.NewDecoder(r).Decode(setup); err != nil {
				log.Println(err)
				return false
			}
		}
		if setup.Fuse < 3 {
			log.Println("invalid file descriptor", setup.Fuse)
			return false
		}
		return true
	}

	if unsafeOpts.source == nil {
		showHelp(args)
		return false
	} else if a, err := check.NewAbs(C.GoString(unsafeOpts.source)); err != nil {
		log.Println(err)
		return false
	} else {
		setup.Source = a
	}
	setup.mkdir = unsafeOpts.mkdir != 0

	if unsafeOpts.setuid == nil {
		setup.Setuid = -1
	} else if v, err := strconv.Atoi(C.GoString(unsafeOpts.setuid)); err != nil || v <= 0 {
		log.Println("invalid value for option setuid")
		return false
	} else {
		setup.Setuid = v
	}
	if unsafeOpts.setgid == nil {
		setup.Setgid = -1
	} else if v, err := strconv.Atoi(C.GoString(unsafeOpts.setgid)); err != nil || v <= 0 {
		log.Println("invalid value for option setgid")
		return false
	} else {
		setup.Setgid = v
	}

	return true
}

// copyArgs returns a heap allocated copy of an argument slice in fuse_args
// representation.
func copyArgs(s ...string) fuseArgs {
	if len(s) == 0 {
		return fuseArgs{argc: 0, argv: nil, allocated: 0}
	}
	args := unsafe.Slice((**C.char)(C.malloc(C.size_t(uintptr(len(s))*unsafe.Sizeof(s[0])))), len(s))
	for i, arg := range s {
		args[i] = C.CString(arg)
	}
	return fuseArgs{argc: C.int(len(s)), argv: &args[0], allocated: 1}
}

// freeArgs frees the contents of argument list.
func freeArgs(args *fuseArgs) { C.fuse_opt_free_args(args) }

// unsafeAddArgument adds an argument to fuseArgs via fuse_opt_add_arg.
//
// The last byte of arg must be 0.
func unsafeAddArgument(args *fuseArgs, arg string) {
	C.fuse_opt_add_arg(args, (*C.char)(unsafe.Pointer(unsafe.StringData(arg))))
}

func _main(s ...string) (exitCode int) {
	msg := message.New(log.Default())
	runtime.LockOSThread()

	// don't mask creation mode, kernel already did that
	syscall.Umask(0)

	var pinner runtime.Pinner
	defer pinner.Unpin()

	args := copyArgs(s...)
	defer freeArgs(&args)

	// this causes the kernel to enforce access control based on struct stat
	// populated by sharefs_getattr
	unsafeAddArgument(&args, "-odefault_permissions\x00")

	var priv C.struct_sharefs_private
	pinner.Pin(&priv)
	var setup setupState
	priv.setup = C.uintptr_t(cgo.NewHandle(&setup))
	defer destroySetup(unsafe.Pointer(&priv))

	var opts C.struct_fuse_cmdline_opts
	if C.fuse_parse_cmdline(&args, &opts) != 0 {
		return 1
	}
	if opts.mountpoint != nil {
		defer C.free(unsafe.Pointer(opts.mountpoint))
	}

	if opts.show_version != 0 {
		fmt.Println("hakurei version", info.Version())
		fmt.Println("FUSE library version", C.GoString(C.fuse_pkgversion()))
		C.fuse_lowlevel_version()
		return 0
	}

	if opts.show_help != 0 {
		showHelp(&args)
		return 0
	} else if opts.mountpoint == nil {
		log.Println("no mountpoint specified")
		return 2
	} else {
		// hack to keep fuse_parse_cmdline happy in the container
		mountpoint := C.GoString(opts.mountpoint)
		pathnameArg := -1
		for i, arg := range s {
			if arg == mountpoint {
				pathnameArg = i
				break
			}
		}
		if pathnameArg < 0 {
			log.Println("mountpoint must be absolute")
			return 2
		}
		s[pathnameArg] = container.Nonexistent
	}

	if !parseOpts(&args, &setup, msg.GetLogger()) {
		return 1
	}
	asRoot := os.Geteuid() == 0

	if asRoot {
		if setup.Setuid <= 0 || setup.Setgid <= 0 {
			log.Println("setuid and setgid must not be 0")
			return 1
		}

		if setup.Fuse >= 3 {
			log.Println("filesystem daemon must not run as root")
			return 1
		}

		if setup.mkdir {
			if err := os.MkdirAll(setup.Source.String(), 0700); err != nil {
				if !errors.Is(err, os.ErrExist) {
					log.Println(err)
					return 1
				}
				// skip setup for existing source directory
			} else if err = os.Chown(setup.Source.String(), setup.Setuid, setup.Setgid); err != nil {
				log.Println(err)
				return 1
			}
		}
	} else if setup.Fuse < 3 && (setup.Setuid > 0 || setup.Setgid > 0) {
		log.Println("setuid and setgid has no effect when not starting as root")
		return 1
	} else if setup.mkdir {
		log.Println("mkdir has no effect when not starting as root")
		return 1
	}

	op := C.struct_fuse_operations{
		init:    closure(C.sharefs_init),
		destroy: closure(C.sharefs_destroy),

		// implemented in fuse-helper.c
		getattr:  closure(C.sharefs_getattr),
		readdir:  closure(C.sharefs_readdir),
		mkdir:    closure(C.sharefs_mkdir),
		unlink:   closure(C.sharefs_unlink),
		rmdir:    closure(C.sharefs_rmdir),
		rename:   closure(C.sharefs_rename),
		truncate: closure(C.sharefs_truncate),
		utimens:  closure(C.sharefs_utimens),
		create:   closure(C.sharefs_create),
		open:     closure(C.sharefs_open),
		read:     closure(C.sharefs_read),
		write:    closure(C.sharefs_write),
		statfs:   closure(C.sharefs_statfs),
		release:  closure(C.sharefs_release),
		fsync:    closure(C.sharefs_fsync),
	}

	fuse := C.fuse_new_fn(&args, &op, C.size_t(unsafe.Sizeof(op)), unsafe.Pointer(&priv))
	if fuse == nil {
		return 3
	}
	defer C.fuse_destroy(fuse)
	se := C.fuse_get_session(fuse)

	if setup.Fuse < 3 {
		// unconfined, set up mount point and container
		if C.fuse_mount(fuse, opts.mountpoint) != 0 {
			return 4
		}
		// unmounted by initial process
		defer func() {
			if exitCode == 5 {
				C.fuse_unmount(fuse)
			}
		}()

		if asRoot {
			if err := syscall.Setresgid(setup.Setgid, setup.Setgid, setup.Setgid); err != nil {
				log.Printf("cannot set gid: %v", err)
				return 5
			}
			if err := syscall.Setgroups(nil); err != nil {
				log.Printf("cannot set supplementary groups: %v", err)
				return 5
			}
			if err := syscall.Setresuid(setup.Setuid, setup.Setuid, setup.Setuid); err != nil {
				log.Printf("cannot set uid: %v", err)
				return 5
			}
		}

		msg.SwapVerbose(opts.debug != 0)
		ctx := context.Background()
		if opts.foreground != 0 {
			c, cancel := signal.NotifyContext(ctx, syscall.SIGINT, syscall.SIGTERM)
			defer cancel()
			ctx = c
		}
		z := container.New(ctx, msg)
		z.AllowOrphan = opts.foreground == 0
		z.Env = os.Environ()

		// keep fuse_parse_cmdline happy in the container
		z.Tmpfs(check.MustAbs(container.Nonexistent), 1<<10, 0755)

		z.Path = fhs.AbsProcSelfExe
		z.Args = s
		z.ForwardCancel = true
		z.SeccompPresets |= std.PresetStrict
		z.ParentPerm = 0700
		z.Bind(setup.Source, setup.Source, std.BindWritable)
		if !z.AllowOrphan {
			z.WaitDelay = hst.WaitDelayMax
			z.Stdin, z.Stdout, z.Stderr = os.Stdin, os.Stdout, os.Stderr
		}
		z.Bind(z.Path, z.Path, 0)
		setup.Fuse = int(proc.ExtraFileSlice(
			&z.ExtraFiles,
			os.NewFile(uintptr(C.fuse_session_fd(se)), "fuse"),
		))

		var setupPipe [2]*os.File
		if r, w, err := os.Pipe(); err != nil {
			log.Println(err)
			return 5
		} else {
			z.Args = append(z.Args, "-osetup="+strconv.Itoa(3+len(z.ExtraFiles)))
			z.ExtraFiles = append(z.ExtraFiles, r)
			setupPipe[0], setupPipe[1] = r, w
		}

		if err := z.Start(); err != nil {
			if m, ok := message.GetMessage(err); ok {
				log.Println(m)
			} else {
				log.Println(err)
			}
			return 5
		}
		if err := setupPipe[0].Close(); err != nil {
			log.Println(err)
		}
		if err := z.Serve(); err != nil {
			if m, ok := message.GetMessage(err); ok {
				log.Println(m)
			} else {
				log.Println(err)
			}
			return 5
		}

		if err := gob.NewEncoder(setupPipe[1]).Encode(&setup); err != nil {
			log.Println(err)
			return 5
		} else if err = setupPipe[1].Close(); err != nil {
			log.Println(err)
		}

		if !z.AllowOrphan {
			if err := z.Wait(); err != nil {
				exitError, ok := errors.AsType[*exec.ExitError](err)
				if !ok || exitError == nil {
					log.Println(err)
					return 5
				}
				switch code := exitError.ExitCode(); syscall.Signal(code & 0x7f) {
				case syscall.SIGINT:
				case syscall.SIGTERM:

				default:
					return code
				}
			}
		}
		return 0
	} else { // confined
		C.free(unsafe.Pointer(opts.mountpoint))
		// must be heap allocated
		opts.mountpoint = C.CString("/dev/fd/" + strconv.Itoa(setup.Fuse))

		if err := os.Chdir("/"); err != nil {
			log.Println(err)
		}
	}

	if C.fuse_mount(fuse, opts.mountpoint) != 0 {
		return 4
	}
	defer C.fuse_unmount(fuse)

	if C.fuse_set_signal_handlers(se) != 0 {
		return 6
	}
	defer C.fuse_remove_signal_handlers(se)

	if opts.singlethread != 0 {
		if C.fuse_loop(fuse) != 0 {
			return 8
		}
	} else {
		loopConfig := C.fuse_loop_cfg_create()
		if loopConfig == nil {
			return 7
		}
		defer C.fuse_loop_cfg_destroy(loopConfig)

		C.fuse_loop_cfg_set_clone_fd(loopConfig, C.uint(opts.clone_fd))

		C.fuse_loop_cfg_set_idle_threads(loopConfig, opts.max_idle_threads)
		C.fuse_loop_cfg_set_max_threads(loopConfig, opts.max_threads)
		if C.fuse_loop_mt(fuse, loopConfig) != 0 {
			return 8
		}
	}

	if setup.initFailed {
		return 1
	}
	return 0
}
