//go:build unix

package workspace

import (
	"errors"
	"io/fs"
	"os"
	"path"
	"strings"
	"syscall"

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

func platformFileIdentity(info fs.FileInfo) (uint64, uint64, bool) {
	stat, ok := info.Sys().(*syscall.Stat_t)
	if !ok {
		return 0, 0, false
	}
	return uint64(stat.Dev), uint64(stat.Ino), true
}

// writeParent pins one validated directory descriptor. All write effects and
// the final rename are relative to this descriptor, never to a re-resolved path.
type writeParent struct {
	entry    *catalogEntry
	relative string
	fd       int
	device   uint64
	inode    uint64
}

func openOrCreateParentNoFollow(entry *catalogEntry, name string) (*writeParent, string, error) {
	if entry == nil || entry.directory == nil {
		return nil, "", errUnsafePath
	}
	parent, err := openParentNoFollow(entry, path.Dir(name), true)
	if err != nil {
		return nil, "", err
	}
	return parent, path.Base(name), nil
}

func openParentNoFollow(entry *catalogEntry, relative string, create bool) (*writeParent, error) {
	fd, err := unix.Dup(int(entry.directory.Fd()))
	if err != nil {
		return nil, errUnsafePath
	}
	unix.CloseOnExec(fd)
	closeFD := true
	defer func() {
		if closeFD {
			_ = unix.Close(fd)
		}
	}()

	device, inode, err := directoryIdentity(fd, entry.device)
	if err != nil || device != entry.device || inode != entry.inode {
		return nil, errUnsafePath
	}
	if relative != "." {
		for _, component := range strings.Split(relative, "/") {
			next, openErr := unix.Openat(fd, component, unix.O_RDONLY|unix.O_DIRECTORY|unix.O_NOFOLLOW|unix.O_CLOEXEC, 0)
			if openErr != nil && create && errors.Is(openErr, unix.ENOENT) {
				if err := unix.Mkdirat(fd, component, 0o700); err != nil && !errors.Is(err, unix.EEXIST) {
					return nil, errUnsafePath
				}
				next, openErr = unix.Openat(fd, component, unix.O_RDONLY|unix.O_DIRECTORY|unix.O_NOFOLLOW|unix.O_CLOEXEC, 0)
			}
			if openErr != nil {
				return nil, errUnsafePath
			}
			nextDevice, nextInode, identityErr := directoryIdentity(next, entry.device)
			if identityErr != nil {
				_ = unix.Close(next)
				return nil, errUnsafePath
			}
			_ = unix.Close(fd)
			fd, device, inode = next, nextDevice, nextInode
		}
	}
	closeFD = false
	return &writeParent{entry: entry, relative: relative, fd: fd, device: device, inode: inode}, nil
}

func directoryIdentity(fd int, expectedDevice uint64) (uint64, uint64, error) {
	var stat unix.Stat_t
	if err := unix.Fstat(fd, &stat); err != nil || stat.Mode&unix.S_IFMT != unix.S_IFDIR {
		return 0, 0, errUnsafePath
	}
	device := uint64(stat.Dev)
	if device != expectedDevice {
		return 0, 0, errUnsafePath
	}
	return device, uint64(stat.Ino), nil
}

func (p *writeParent) close() error {
	if p == nil || p.fd < 0 {
		return nil
	}
	err := unix.Close(p.fd)
	p.fd = -1
	return err
}

func (p *writeParent) targetIdentity(base string) (targetIdentity, error) {
	var stat unix.Stat_t
	err := unix.Fstatat(p.fd, base, &stat, unix.AT_SYMLINK_NOFOLLOW)
	if errors.Is(err, unix.ENOENT) {
		return targetIdentity{}, nil
	}
	if err != nil {
		return targetIdentity{}, errUnsafePath
	}
	return targetIdentity{
		exists: true,
		device: uint64(stat.Dev),
		inode:  uint64(stat.Ino),
		mode:   unixFileMode(uint32(stat.Mode)),
	}, nil
}

func unixFileMode(mode uint32) fs.FileMode {
	permissions := fs.FileMode(mode & 0o777)
	switch mode & unix.S_IFMT {
	case unix.S_IFDIR:
		return permissions | fs.ModeDir
	case unix.S_IFLNK:
		return permissions | fs.ModeSymlink
	case unix.S_IFIFO:
		return permissions | fs.ModeNamedPipe
	case unix.S_IFSOCK:
		return permissions | fs.ModeSocket
	case unix.S_IFCHR:
		return permissions | fs.ModeDevice | fs.ModeCharDevice
	case unix.S_IFBLK:
		return permissions | fs.ModeDevice
	case unix.S_IFREG:
		return permissions
	default:
		return permissions | fs.ModeIrregular
	}
}

func (p *writeParent) createTemp(base string) (*os.File, error) {
	fd, err := unix.Openat(p.fd, base, unix.O_WRONLY|unix.O_CREAT|unix.O_EXCL|unix.O_NOFOLLOW|unix.O_CLOEXEC, 0o600)
	if err != nil {
		return nil, errUnsafePath
	}
	return os.NewFile(uintptr(fd), base), nil
}

func (p *writeParent) remove(base string) error {
	if err := unix.Unlinkat(p.fd, base, 0); err != nil && !errors.Is(err, unix.ENOENT) {
		return errUnsafePath
	}
	return nil
}

func (p *writeParent) rename(oldBase, newBase string) error {
	if err := unix.Renameat(p.fd, oldBase, p.fd, newBase); err != nil {
		return errUnsafePath
	}
	return nil
}

func (p *writeParent) sync() error {
	if err := unix.Fsync(p.fd); err != nil {
		return errUnsafePath
	}
	return nil
}

func (p *writeParent) revalidate() error {
	current, err := openParentNoFollow(p.entry, p.relative, false)
	if err != nil {
		return errUnsafePath
	}
	defer current.close()
	if current.device != p.device || current.inode != p.inode {
		return errUnsafePath
	}
	return nil
}
