package workspace

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

var (
	errInvalidPath  = errors.New("workspace path is invalid")
	errReservedPath = errors.New("workspace path is reserved")
	errUnsafePath   = errors.New("workspace path is unsafe")
	errNotFound     = errors.New("workspace path not found")
)

type targetIdentity struct {
	exists bool
	device uint64
	inode  uint64
	mode   fs.FileMode
}

// userPath accepts a canonical relative path only. .iop is private runtime
// state: no caller-facing operation can name it or a child beneath it.
func userPath(value string) (string, error) {
	if value == "" || strings.Contains(value, "\\") || path.IsAbs(value) || path.Clean(value) != value {
		return "", errInvalidPath
	}
	if value == "." {
		return value, nil
	}
	if strings.HasPrefix(value, "../") || value == ".." {
		return "", errInvalidPath
	}
	first := strings.Split(value, "/")[0]
	if first == ".iop" {
		return "", errReservedPath
	}
	return value, nil
}

// internalPath is deliberately unexported. It is available only to future
// request-owned runtime artifacts and cannot name sibling request namespaces.
func (r *Request) internalPath(value string) (string, error) {
	if value == "" || path.IsAbs(value) || path.Clean(value) != value || value == "." || strings.HasPrefix(value, "../") || value == ".." {
		return "", errInvalidPath
	}
	prefix := r.internalPrefix + "/"
	if value != r.internalPrefix && !strings.HasPrefix(value, prefix) {
		return "", errReservedPath
	}
	return value, nil
}

func checkedExisting(entry *catalogEntry, name string, wantDirectory bool, allowSymlinkTarget bool) (fs.FileInfo, error) {
	if name != "." {
		parts := strings.Split(name, "/")
		for index := range parts {
			partial := strings.Join(parts[:index+1], "/")
			info, err := entry.root.Lstat(partial)
			if err != nil {
				if errors.Is(err, os.ErrNotExist) {
					return nil, errNotFound
				}
				return nil, errUnsafePath
			}
			if info.Mode()&os.ModeSymlink != 0 && (!allowSymlinkTarget || index != len(parts)-1) {
				return nil, errUnsafePath
			}
			if index != len(parts)-1 && !info.IsDir() {
				return nil, errUnsafePath
			}
			if info.Mode()&os.ModeSymlink == 0 {
				if err := sameFilesystem(entry, partial); err != nil {
					return nil, err
				}
			}
		}
	}
	info, err := entry.root.Lstat(name)
	if err != nil {
		if errors.Is(err, os.ErrNotExist) {
			return nil, errNotFound
		}
		return nil, errUnsafePath
	}
	if info.Mode()&os.ModeSymlink != 0 && !allowSymlinkTarget {
		return nil, errUnsafePath
	}
	if !allowSymlinkTarget || info.Mode()&os.ModeSymlink == 0 {
		if err := sameFilesystem(entry, name); err != nil {
			return nil, err
		}
	}
	if wantDirectory && !info.IsDir() {
		return nil, errUnsafePath
	}
	return info, nil
}

func sameFilesystem(entry *catalogEntry, name string) error {
	info, err := entry.root.Stat(name)
	if err != nil {
		return errUnsafePath
	}
	device, _, ok := fileIdentity(info)
	if !ok || device != entry.device {
		return errUnsafePath
	}
	return nil
}
