//go:build darwin || linux

package workspace

import (
	"errors"
	"io"
	"os"
	"path"
	"strings"

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

func initializeRequestArtifacts(entry *catalogEntry, requestID string) (map[string]ownedArtifact, []ownedArtifact, error) {
	if entry == nil || entry.directory == nil || !validRequestID(requestID) {
		return nil, nil, errUnsafePath
	}
	fd, err := duplicateDirectory(entry.directory)
	if err != nil {
		return nil, nil, err
	}
	defer unix.Close(fd)

	components := []string{".iop", "job", requestID}
	created := make([]ownedArtifact, 0, len(components))
	relative := ""
	current := fd
	for index, component := range components {
		if relative == "" {
			relative = component
		} else {
			relative = path.Join(relative, component)
		}
		stat, statErr := statNoFollow(current, component)
		wasCreated := false
		if errors.Is(statErr, unix.ENOENT) {
			if err := unix.Mkdirat(current, component, 0o700); err != nil {
				rollbackOwnedArtifacts(entry, created)
				return nil, nil, errUnsafePath
			}
			wasCreated = true
			stat, statErr = statNoFollow(current, component)
		}
		if statErr != nil || stat.Mode&unix.S_IFMT != unix.S_IFDIR || uint64(stat.Dev) != entry.device || index == len(components)-1 && !wasCreated {
			rollbackOwnedArtifacts(entry, created)
			return nil, nil, errUnsafePath
		}
		next, openErr := openDirectoryAt(current, component, entry.device)
		if openErr != nil {
			rollbackOwnedArtifacts(entry, created)
			return nil, nil, openErr
		}
		opened, identityErr := descriptorArtifact(next, relative, ownedArtifactDirectory, entry.device)
		if identityErr != nil || opened.device != uint64(stat.Dev) || opened.inode != uint64(stat.Ino) {
			unix.Close(next)
			rollbackOwnedArtifacts(entry, created)
			return nil, nil, errUnsafePath
		}
		if wasCreated {
			created = append(created, opened)
		}
		if current != fd {
			unix.Close(current)
		}
		current = next
	}
	if current != fd {
		unix.Close(current)
	}
	requestRoot := ".iop/job/" + requestID
	artifacts := map[string]ownedArtifact{requestRoot: created[len(created)-1]}
	parents := make([]ownedArtifact, 0, 2)
	for _, artifact := range created[:len(created)-1] {
		parents = append(parents, artifact)
	}
	return artifacts, parents, nil
}

func createOwnedArtifact(entry *catalogEntry, requestRoot, relative string, content []byte, inventory map[string]ownedArtifact) ([]ownedArtifact, error) {
	rootIdentity, ok := inventory[requestRoot]
	if !ok || rootIdentity.kind != ownedArtifactDirectory {
		return nil, errUnsafePath
	}
	fd, err := openDirectoryPath(entry, requestRoot)
	if err != nil {
		return nil, err
	}
	defer unix.Close(fd)
	currentIdentity, err := descriptorArtifact(fd, requestRoot, ownedArtifactDirectory, entry.device)
	if err != nil || currentIdentity != rootIdentity {
		return nil, errUnsafePath
	}

	parts := strings.Split(relative, "/")
	created := make([]ownedArtifact, 0, len(parts))
	currentPath := requestRoot
	current := fd
	for _, component := range parts[:len(parts)-1] {
		currentPath = path.Join(currentPath, component)
		stat, statErr := statNoFollow(current, component)
		if errors.Is(statErr, unix.ENOENT) {
			if err := unix.Mkdirat(current, component, 0o700); err != nil {
				rollbackOwnedArtifacts(entry, created)
				return nil, errUnsafePath
			}
			stat, statErr = statNoFollow(current, component)
			if statErr != nil {
				rollbackOwnedArtifacts(entry, created)
				return nil, errUnsafePath
			}
			created = append(created, ownedArtifact{relative: currentPath, kind: ownedArtifactDirectory, device: uint64(stat.Dev), inode: uint64(stat.Ino)})
		}
		owned, admitted := inventory[currentPath]
		if !admitted {
			for _, candidate := range created {
				if candidate.relative == currentPath {
					owned, admitted = candidate, true
					break
				}
			}
		}
		if statErr != nil || stat.Mode&unix.S_IFMT != unix.S_IFDIR || uint64(stat.Dev) != entry.device || !admitted || owned.kind != ownedArtifactDirectory || owned.device != uint64(stat.Dev) || owned.inode != uint64(stat.Ino) {
			rollbackOwnedArtifacts(entry, created)
			return nil, errUnsafePath
		}
		next, openErr := openDirectoryAt(current, component, entry.device)
		if openErr != nil {
			rollbackOwnedArtifacts(entry, created)
			return nil, openErr
		}
		if current != fd {
			unix.Close(current)
		}
		current = next
	}
	if current != fd {
		defer unix.Close(current)
	}

	base := parts[len(parts)-1]
	if _, statErr := statNoFollow(current, base); !errors.Is(statErr, unix.ENOENT) {
		rollbackOwnedArtifacts(entry, created)
		return nil, errUnsafePath
	}
	fileFD, err := unix.Openat(current, base, unix.O_WRONLY|unix.O_CREAT|unix.O_EXCL|unix.O_NOFOLLOW|unix.O_CLOEXEC, 0o600)
	if err != nil {
		rollbackOwnedArtifacts(entry, created)
		return nil, errUnsafePath
	}
	file := os.NewFile(uintptr(fileFD), base)
	written := false
	defer func() {
		if !written {
			_ = unix.Unlinkat(current, base, 0)
		}
	}()
	if _, err := file.Write(content); err != nil {
		file.Close()
		rollbackOwnedArtifacts(entry, created)
		return nil, errUnsafePath
	}
	if err := file.Sync(); err != nil {
		file.Close()
		rollbackOwnedArtifacts(entry, created)
		return nil, errUnsafePath
	}
	var stat unix.Stat_t
	if err := unix.Fstat(fileFD, &stat); err != nil || stat.Mode&unix.S_IFMT != unix.S_IFREG || uint64(stat.Dev) != entry.device {
		file.Close()
		rollbackOwnedArtifacts(entry, created)
		return nil, errUnsafePath
	}
	if err := file.Close(); err != nil {
		rollbackOwnedArtifacts(entry, created)
		return nil, errUnsafePath
	}
	written = true
	created = append(created, ownedArtifact{
		relative: path.Join(requestRoot, relative), kind: ownedArtifactFile,
		device: uint64(stat.Dev), inode: uint64(stat.Ino),
	})
	return created, nil
}

func readOwnedArtifact(entry *catalogEntry, requestRoot, relative string, inventory map[string]ownedArtifact, maximum int) ([]byte, error) {
	if entry == nil || maximum < 1 {
		return nil, errUnsafePath
	}
	artifactPath := path.Join(requestRoot, relative)
	owned, admitted := inventory[artifactPath]
	if !admitted {
		return nil, os.ErrNotExist
	}
	if owned.kind != ownedArtifactFile {
		return nil, errUnsafePath
	}
	parentPath, base := path.Dir(artifactPath), path.Base(artifactPath)
	parentOwned, admitted := inventory[parentPath]
	if !admitted || parentOwned.kind != ownedArtifactDirectory {
		return nil, errUnsafePath
	}
	parentFD, err := openDirectoryPath(entry, parentPath)
	if err != nil {
		return nil, err
	}
	defer unix.Close(parentFD)
	openedParent, err := descriptorArtifact(parentFD, parentPath, ownedArtifactDirectory, entry.device)
	if err != nil || openedParent != parentOwned {
		return nil, errUnsafePath
	}
	stat, err := statNoFollow(parentFD, base)
	if err != nil || stat.Mode&unix.S_IFMT != unix.S_IFREG || uint64(stat.Dev) != owned.device || uint64(stat.Ino) != owned.inode || stat.Size < 0 || stat.Size > int64(maximum) {
		return nil, errUnsafePath
	}
	fileFD, err := unix.Openat(parentFD, base, unix.O_RDONLY|unix.O_NOFOLLOW|unix.O_CLOEXEC, 0)
	if err != nil {
		return nil, errUnsafePath
	}
	file := os.NewFile(uintptr(fileFD), base)
	defer file.Close()
	opened, err := descriptorArtifact(fileFD, artifactPath, ownedArtifactFile, entry.device)
	if err != nil || opened != owned {
		return nil, errUnsafePath
	}
	content, err := io.ReadAll(io.LimitReader(file, int64(maximum)+1))
	if err != nil || len(content) > maximum {
		return nil, errUnsafePath
	}
	var after unix.Stat_t
	if err := unix.Fstat(fileFD, &after); err != nil || after.Mode&unix.S_IFMT != unix.S_IFREG || uint64(after.Dev) != owned.device || uint64(after.Ino) != owned.inode || after.Size != int64(len(content)) {
		return nil, errUnsafePath
	}
	return content, nil
}

func validateAndRemoveOwnedArtifacts(entry *catalogEntry, requestRoot string, inventory map[string]ownedArtifact, ownedParents []ownedArtifact) (int, error) {
	if err := validateOwnedTree(entry, requestRoot, inventory); err != nil {
		return 0, err
	}
	removed := 0
	for _, artifact := range sortedArtifactsDeepestFirst(inventory) {
		if err := removeExactArtifact(entry, artifact, false); err != nil {
			return removed, err
		}
		removed++
	}
	for index := len(ownedParents) - 1; index >= 0; index-- {
		if err := removeExactArtifact(entry, ownedParents[index], true); err != nil {
			return removed, err
		}
	}
	return removed, nil
}

func validateOwnedTree(entry *catalogEntry, requestRoot string, inventory map[string]ownedArtifact) error {
	rootIdentity, ok := inventory[requestRoot]
	if !ok || rootIdentity.kind != ownedArtifactDirectory {
		return errUnsafePath
	}
	fd, err := openDirectoryPath(entry, requestRoot)
	if err != nil {
		return err
	}
	defer unix.Close(fd)
	opened, err := descriptorArtifact(fd, requestRoot, ownedArtifactDirectory, entry.device)
	if err != nil || opened != rootIdentity {
		return errUnsafePath
	}
	seen := map[string]struct{}{requestRoot: {}}
	if err := enumerateOwnedDirectory(entry, fd, requestRoot, inventory, seen); err != nil {
		return err
	}
	if len(seen) != len(inventory) {
		return errUnsafePath
	}
	return nil
}

func enumerateOwnedDirectory(entry *catalogEntry, fd int, relative string, inventory map[string]ownedArtifact, seen map[string]struct{}) error {
	dup, err := unix.Dup(fd)
	if err != nil {
		return errUnsafePath
	}
	unix.CloseOnExec(dup)
	directory := os.NewFile(uintptr(dup), relative)
	defer directory.Close()
	for {
		entries, readErr := directory.ReadDir(128)
		for _, entryValue := range entries {
			name := entryValue.Name()
			if name == "" || name == "." || name == ".." || strings.Contains(name, "/") {
				return errUnsafePath
			}
			childPath := path.Join(relative, name)
			stat, err := statNoFollow(fd, name)
			if err != nil || uint64(stat.Dev) != entry.device {
				return errUnsafePath
			}
			kind := ownedArtifactFile
			switch stat.Mode & unix.S_IFMT {
			case unix.S_IFREG:
			case unix.S_IFDIR:
				kind = ownedArtifactDirectory
			default:
				return errUnsafePath
			}
			owned, ok := inventory[childPath]
			if !ok || owned.kind != kind || owned.device != uint64(stat.Dev) || owned.inode != uint64(stat.Ino) {
				return errUnsafePath
			}
			seen[childPath] = struct{}{}
			if kind == ownedArtifactDirectory {
				childFD, err := openDirectoryAt(fd, name, entry.device)
				if err != nil {
					return err
				}
				opened, identityErr := descriptorArtifact(childFD, childPath, kind, entry.device)
				if identityErr != nil || opened != owned {
					unix.Close(childFD)
					return errUnsafePath
				}
				err = enumerateOwnedDirectory(entry, childFD, childPath, inventory, seen)
				unix.Close(childFD)
				if err != nil {
					return err
				}
			}
		}
		if errors.Is(readErr, io.EOF) {
			break
		}
		if readErr != nil {
			return errUnsafePath
		}
	}
	return nil
}

func removeExactArtifact(entry *catalogEntry, artifact ownedArtifact, ignoreNonEmpty bool) error {
	parentPath := path.Dir(artifact.relative)
	base := path.Base(artifact.relative)
	parentFD, err := openDirectoryPath(entry, parentPath)
	if err != nil {
		return err
	}
	defer unix.Close(parentFD)
	stat, err := statNoFollow(parentFD, base)
	if err != nil || uint64(stat.Dev) != artifact.device || uint64(stat.Ino) != artifact.inode {
		return errUnsafePath
	}
	flags := 0
	wantMode := uint32(unix.S_IFREG)
	if artifact.kind == ownedArtifactDirectory {
		flags = unix.AT_REMOVEDIR
		wantMode = unix.S_IFDIR
	}
	if uint32(stat.Mode)&uint32(unix.S_IFMT) != wantMode {
		return errUnsafePath
	}
	if err := unix.Unlinkat(parentFD, base, flags); err != nil {
		if ignoreNonEmpty && (errors.Is(err, unix.ENOTEMPTY) || errors.Is(err, unix.EEXIST)) {
			return nil
		}
		return errUnsafePath
	}
	return nil
}

func rollbackOwnedArtifacts(entry *catalogEntry, artifacts []ownedArtifact) {
	for index := len(artifacts) - 1; index >= 0; index-- {
		_ = removeExactArtifact(entry, artifacts[index], true)
	}
}

func duplicateDirectory(directory *os.File) (int, error) {
	if directory == nil {
		return -1, errUnsafePath
	}
	fd, err := unix.Dup(int(directory.Fd()))
	if err != nil {
		return -1, errUnsafePath
	}
	unix.CloseOnExec(fd)
	return fd, nil
}

func openDirectoryPath(entry *catalogEntry, relative string) (int, error) {
	fd, err := duplicateDirectory(entry.directory)
	if err != nil {
		return -1, err
	}
	if relative == "." {
		return fd, nil
	}
	for _, component := range strings.Split(relative, "/") {
		next, openErr := openDirectoryAt(fd, component, entry.device)
		unix.Close(fd)
		if openErr != nil {
			return -1, openErr
		}
		fd = next
	}
	return fd, nil
}

func openDirectoryAt(parent int, name string, device uint64) (int, error) {
	fd, err := unix.Openat(parent, name, unix.O_RDONLY|unix.O_DIRECTORY|unix.O_NOFOLLOW|unix.O_CLOEXEC, 0)
	if err != nil {
		return -1, errUnsafePath
	}
	var stat unix.Stat_t
	if err := unix.Fstat(fd, &stat); err != nil || stat.Mode&unix.S_IFMT != unix.S_IFDIR || uint64(stat.Dev) != device {
		unix.Close(fd)
		return -1, errUnsafePath
	}
	return fd, nil
}

func statNoFollow(parent int, name string) (unix.Stat_t, error) {
	var stat unix.Stat_t
	err := unix.Fstatat(parent, name, &stat, unix.AT_SYMLINK_NOFOLLOW)
	return stat, err
}

func descriptorArtifact(fd int, relative string, kind ownedArtifactKind, device uint64) (ownedArtifact, error) {
	var stat unix.Stat_t
	if err := unix.Fstat(fd, &stat); err != nil || uint64(stat.Dev) != device {
		return ownedArtifact{}, errUnsafePath
	}
	wantMode := uint32(unix.S_IFREG)
	if kind == ownedArtifactDirectory {
		wantMode = unix.S_IFDIR
	}
	if uint32(stat.Mode)&uint32(unix.S_IFMT) != wantMode {
		return ownedArtifact{}, errUnsafePath
	}
	return ownedArtifact{relative: relative, kind: kind, device: uint64(stat.Dev), inode: uint64(stat.Ino)}, nil
}
