//go:build unix

package contentpayload

import (
	"errors"
	"fmt"
	"os"
	"strings"
	"syscall"

	"golang.org/x/sys/unix"
)

func openRoot(root string) (*os.File, error) {
	fd, err := unix.Open(root, unix.O_RDONLY|unix.O_DIRECTORY|unix.O_CLOEXEC|unix.O_NOFOLLOW, 0)
	if err != nil {
		return nil, fmt.Errorf("open content storage root: %w", err)
	}
	return os.NewFile(uintptr(fd), root), nil
}

func directoryOwnedByProcess(info os.FileInfo) bool {
	stat, ok := info.Sys().(*syscall.Stat_t)
	return ok && stat.Uid == uint32(os.Geteuid())
}

func openAbsoluteDirectory(path string) (*os.File, error) {
	if !strings.HasPrefix(path, "/") {
		return nil, fmt.Errorf("absolute directory is required")
	}
	root, err := openRoot("/")
	if err != nil {
		return nil, err
	}
	defer root.Close()
	trimmed := strings.Trim(path, "/")
	if trimmed == "" {
		return openDirectoryAt(root, nil, false)
	}
	return openDirectoryAt(root, strings.Split(trimmed, "/"), false)
}

// openDirectoryAt walks from a pinned directory fd. Every component is opened
// with O_NOFOLLOW, so concurrent symlink replacement cannot redirect traversal.
func openDirectoryAt(root *os.File, components []string, create bool) (*os.File, error) {
	current, err := unix.Dup(int(root.Fd()))
	if err != nil {
		return nil, fmt.Errorf("duplicate content directory: %w", err)
	}
	for _, component := range components {
		next, openErr := unix.Openat(current, component, unix.O_RDONLY|unix.O_DIRECTORY|unix.O_CLOEXEC|unix.O_NOFOLLOW, 0)
		if errors.Is(openErr, syscall.ENOENT) && create {
			if mkdirErr := unix.Mkdirat(current, component, 0o750); mkdirErr != nil && !errors.Is(mkdirErr, syscall.EEXIST) {
				unix.Close(current)
				return nil, fmt.Errorf("create content directory %q: %w", component, mkdirErr)
			}
			next, openErr = unix.Openat(current, component, unix.O_RDONLY|unix.O_DIRECTORY|unix.O_CLOEXEC|unix.O_NOFOLLOW, 0)
		}
		if openErr != nil {
			unix.Close(current)
			return nil, fmt.Errorf("open content directory %q: %w", component, openErr)
		}
		unix.Close(current)
		current = next
	}
	return os.NewFile(uintptr(current), "content-directory"), nil
}

func openRegularAt(directory *os.File, name string, flags int, mode os.FileMode) (*os.File, error) {
	// O_NONBLOCK prevents a substituted FIFO or device from hanging a
	// security check before fstat can reject the non-regular entry. It has no
	// effect on ordinary files.
	fd, err := unix.Openat(int(directory.Fd()), name, flags|unix.O_CLOEXEC|unix.O_NOFOLLOW|unix.O_NONBLOCK, uint32(mode.Perm()))
	if err != nil {
		return nil, err
	}
	var stat unix.Stat_t
	if err := unix.Fstat(fd, &stat); err != nil {
		unix.Close(fd)
		return nil, err
	}
	if stat.Mode&unix.S_IFMT != unix.S_IFREG {
		unix.Close(fd)
		return nil, fmt.Errorf("content object is not a regular file")
	}
	return os.NewFile(uintptr(fd), name), nil
}

func unlinkAt(directory *os.File, name string, directoryEntry bool) error {
	flags := 0
	if directoryEntry {
		flags = unix.AT_REMOVEDIR
	}
	return unix.Unlinkat(int(directory.Fd()), name, flags)
}

func renameAt(oldDirectory *os.File, oldName string, newDirectory *os.File, newName string) error {
	err := unix.Renameat(int(oldDirectory.Fd()), oldName, int(newDirectory.Fd()), newName)
	return err
}

func linkAt(oldDirectory *os.File, oldName string, newDirectory *os.File, newName string) error {
	err := unix.Linkat(int(oldDirectory.Fd()), oldName, int(newDirectory.Fd()), newName, 0)
	return err
}

func mkdirAt(directory *os.File, name string, mode os.FileMode) error {
	err := unix.Mkdirat(int(directory.Fd()), name, uint32(mode.Perm()))
	return err
}
