package landlock

import (
	"strings"
	"syscall"
	"unsafe"

	"hakurei.app/ext"
)

// include/uapi/linux/landlock.h

const (
	LANDLOCK_CREATE_RULESET_VERSION = 1 << iota
)

// AccessFS is bitmask of handled filesystem actions.
type AccessFS uint64

const (
	LANDLOCK_ACCESS_FS_EXECUTE AccessFS = 1 << iota
	LANDLOCK_ACCESS_FS_WRITE_FILE
	LANDLOCK_ACCESS_FS_READ_FILE
	LANDLOCK_ACCESS_FS_READ_DIR
	LANDLOCK_ACCESS_FS_REMOVE_DIR
	LANDLOCK_ACCESS_FS_REMOVE_FILE
	LANDLOCK_ACCESS_FS_MAKE_CHAR
	LANDLOCK_ACCESS_FS_MAKE_DIR
	LANDLOCK_ACCESS_FS_MAKE_REG
	LANDLOCK_ACCESS_FS_MAKE_SOCK
	LANDLOCK_ACCESS_FS_MAKE_FIFO
	LANDLOCK_ACCESS_FS_MAKE_BLOCK
	LANDLOCK_ACCESS_FS_MAKE_SYM
	LANDLOCK_ACCESS_FS_REFER
	LANDLOCK_ACCESS_FS_TRUNCATE
	LANDLOCK_ACCESS_FS_IOCTL_DEV

	_LANDLOCK_ACCESS_FS_DELIM
)

// String returns a space-separated string of [AccessFS] flags.
func (f AccessFS) String() string {
	switch f {
	case LANDLOCK_ACCESS_FS_EXECUTE:
		return "execute"

	case LANDLOCK_ACCESS_FS_WRITE_FILE:
		return "write_file"

	case LANDLOCK_ACCESS_FS_READ_FILE:
		return "read_file"

	case LANDLOCK_ACCESS_FS_READ_DIR:
		return "read_dir"

	case LANDLOCK_ACCESS_FS_REMOVE_DIR:
		return "remove_dir"

	case LANDLOCK_ACCESS_FS_REMOVE_FILE:
		return "remove_file"

	case LANDLOCK_ACCESS_FS_MAKE_CHAR:
		return "make_char"

	case LANDLOCK_ACCESS_FS_MAKE_DIR:
		return "make_dir"

	case LANDLOCK_ACCESS_FS_MAKE_REG:
		return "make_reg"

	case LANDLOCK_ACCESS_FS_MAKE_SOCK:
		return "make_sock"

	case LANDLOCK_ACCESS_FS_MAKE_FIFO:
		return "make_fifo"

	case LANDLOCK_ACCESS_FS_MAKE_BLOCK:
		return "make_block"

	case LANDLOCK_ACCESS_FS_MAKE_SYM:
		return "make_sym"

	case LANDLOCK_ACCESS_FS_REFER:
		return "fs_refer"

	case LANDLOCK_ACCESS_FS_TRUNCATE:
		return "fs_truncate"

	case LANDLOCK_ACCESS_FS_IOCTL_DEV:
		return "fs_ioctl_dev"

	default:
		var c []AccessFS
		for i := AccessFS(1); i < _LANDLOCK_ACCESS_FS_DELIM; i <<= 1 {
			if f&i != 0 {
				c = append(c, i)
			}
		}
		if len(c) == 0 {
			return "NULL"
		}
		s := make([]string, len(c))
		for i, v := range c {
			s[i] = v.String()
		}
		return strings.Join(s, " ")
	}
}

// AccessNet is bitmask of handled network actions.
type AccessNet uint64

const (
	LANDLOCK_ACCESS_NET_BIND_TCP AccessNet = 1 << iota
	LANDLOCK_ACCESS_NET_CONNECT_TCP

	_LANDLOCK_ACCESS_NET_DELIM
)

// String returns a space-separated string of [AccessNet] flags.
func (f AccessNet) String() string {
	switch f {
	case LANDLOCK_ACCESS_NET_BIND_TCP:
		return "bind_tcp"

	case LANDLOCK_ACCESS_NET_CONNECT_TCP:
		return "connect_tcp"

	default:
		var c []AccessNet
		for i := AccessNet(1); i < _LANDLOCK_ACCESS_NET_DELIM; i <<= 1 {
			if f&i != 0 {
				c = append(c, i)
			}
		}
		if len(c) == 0 {
			return "NULL"
		}
		s := make([]string, len(c))
		for i, v := range c {
			s[i] = v.String()
		}
		return strings.Join(s, " ")
	}
}

// Scope is bitmask of scopes restricting a Landlock domain from accessing outside resources.
type Scope uint64

const (
	LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET Scope = 1 << iota
	LANDLOCK_SCOPE_SIGNAL

	_LANDLOCK_SCOPE_DELIM
)

// String returns a space-separated string of [Scope] flags.
func (f Scope) String() string {
	switch f {
	case LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET:
		return "abstract_unix_socket"

	case LANDLOCK_SCOPE_SIGNAL:
		return "signal"

	default:
		var c []Scope
		for i := Scope(1); i < _LANDLOCK_SCOPE_DELIM; i <<= 1 {
			if f&i != 0 {
				c = append(c, i)
			}
		}
		if len(c) == 0 {
			return "NULL"
		}
		s := make([]string, len(c))
		for i, v := range c {
			s[i] = v.String()
		}
		return strings.Join(s, " ")
	}
}

// RulesetAttr is equivalent to struct landlock_ruleset_attr.
type RulesetAttr struct {
	// Bitmask of handled filesystem actions.
	HandledAccessFS AccessFS
	// Bitmask of handled network actions.
	HandledAccessNet AccessNet
	// Bitmask of scopes restricting a Landlock domain from accessing outside
	// resources (e.g. IPCs).
	Scoped Scope
}

// String returns a user-facing description of [RulesetAttr].
func (rulesetAttr *RulesetAttr) String() string {
	if rulesetAttr == nil {
		return "NULL"
	}
	elems := make([]string, 0, 3)
	if rulesetAttr.HandledAccessFS > 0 {
		elems = append(elems, "fs: "+rulesetAttr.HandledAccessFS.String())
	}
	if rulesetAttr.HandledAccessNet > 0 {
		elems = append(elems, "net: "+rulesetAttr.HandledAccessNet.String())
	}
	if rulesetAttr.Scoped > 0 {
		elems = append(elems, "scoped: "+rulesetAttr.Scoped.String())
	}
	if len(elems) == 0 {
		return "0"
	}
	return strings.Join(elems, ", ")
}

// Create loads the ruleset into the kernel.
func (rulesetAttr *RulesetAttr) Create(flags uintptr) (fd int, err error) {
	var pointer, size uintptr
	// NULL needed for abi version
	if rulesetAttr != nil {
		pointer = uintptr(unsafe.Pointer(rulesetAttr))
		size = unsafe.Sizeof(*rulesetAttr)
	}

	rulesetFd, _, errno := syscall.Syscall(
		ext.SYS_LANDLOCK_CREATE_RULESET,
		pointer, size,
		flags,
	)
	fd = int(rulesetFd)
	err = errno
	if fd < 0 {
		return
	}

	if rulesetAttr != nil { // not a fd otherwise
		syscall.CloseOnExec(fd)
	}
	return fd, nil
}

// GetABI returns the ABI version supported by the kernel.
func GetABI() (int, error) {
	return (*RulesetAttr)(nil).Create(LANDLOCK_CREATE_RULESET_VERSION)
}

// RestrictSelf applies a loaded ruleset to the calling thread.
func RestrictSelf(rulesetFd int, flags uintptr) error {
	r, _, errno := syscall.Syscall(
		ext.SYS_LANDLOCK_RESTRICT_SELF,
		uintptr(rulesetFd),
		flags,
		0,
	)
	if r != 0 {
		return errno
	}
	return nil
}
