//go:build unix

package contentpayload

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

	"github.com/go-git/go-billy/v5"
	"github.com/go-git/go-billy/v5/helper/chroot"
	"golang.org/x/sys/unix"
)

type descriptorBasic struct{ root *os.File }
type descriptorFile struct{ *os.File }

func newDescriptorFS(root *os.File) billy.Filesystem {
	base := &descriptorBasic{root: root}
	return chroot.New(base, ".")
}

func descriptorParts(name string) ([]string, error) {
	name = strings.ReplaceAll(strings.TrimSpace(name), "\\", "/")
	if name == "" || name == "." {
		return nil, nil
	}
	if strings.HasPrefix(name, "/") {
		return nil, billy.ErrCrossedBoundary
	}
	clean := path.Clean(name)
	if clean == ".." || strings.HasPrefix(clean, "../") {
		return nil, billy.ErrCrossedBoundary
	}
	return strings.Split(clean, "/"), nil
}

func (fs *descriptorBasic) parent(name string, create bool) (*os.File, string, error) {
	parts, err := descriptorParts(name)
	if err != nil || len(parts) == 0 {
		return nil, "", fmt.Errorf("descriptor file path is invalid")
	}
	parent, err := openDirectoryAt(fs.root, parts[:len(parts)-1], create)
	return parent, parts[len(parts)-1], err
}

func (fs *descriptorBasic) Create(name string) (billy.File, error) {
	file, err := fs.OpenFile(name, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0o666)
	return file, err
}
func (fs *descriptorBasic) Open(name string) (billy.File, error) {
	file, err := fs.OpenFile(name, os.O_RDONLY, 0)
	return file, err
}
func (fs *descriptorBasic) OpenFile(name string, flags int, mode os.FileMode) (billy.File, error) {
	parent, base, err := fs.parent(name, flags&os.O_CREATE != 0)
	if err != nil {
		return nil, err
	}
	defer parent.Close()
	file, err := openRegularAt(parent, base, flags, mode)
	if err != nil {
		return nil, err
	}
	return &descriptorFile{file}, nil
}

func (fs *descriptorBasic) Stat(name string) (os.FileInfo, error)  { return fs.stat(name) }
func (fs *descriptorBasic) Lstat(name string) (os.FileInfo, error) { return fs.stat(name) }
func (fs *descriptorBasic) stat(name string) (os.FileInfo, error) {
	parts, err := descriptorParts(name)
	if err != nil {
		return nil, err
	}
	if directory, openErr := openDirectoryAt(fs.root, parts, false); openErr == nil {
		defer directory.Close()
		return directory.Stat()
	}
	parent, base, err := fs.parent(name, false)
	if err != nil {
		if errors.Is(err, syscall.ENOENT) {
			return nil, os.ErrNotExist
		}
		return nil, err
	}
	defer parent.Close()
	file, err := openRegularAt(parent, base, os.O_RDONLY, 0)
	if err != nil {
		if errors.Is(err, syscall.ENOENT) {
			return nil, os.ErrNotExist
		}
		return nil, err
	}
	defer file.Close()
	return file.Stat()
}

func (fs *descriptorBasic) Rename(from, to string) error {
	oldParent, oldName, err := fs.parent(from, false)
	if err != nil {
		return err
	}
	defer oldParent.Close()
	newParent, newName, err := fs.parent(to, true)
	if err != nil {
		return err
	}
	defer newParent.Close()
	return renameAt(oldParent, oldName, newParent, newName)
}

func (fs *descriptorBasic) Remove(name string) error {
	parent, base, err := fs.parent(name, false)
	if err != nil {
		return err
	}
	defer parent.Close()
	if err := unlinkAt(parent, base, false); err == nil {
		return nil
	}
	return unlinkAt(parent, base, true)
}

func (*descriptorBasic) Join(elements ...string) string { return path.Join(elements...) }

func (fs *descriptorBasic) ReadDir(name string) ([]os.FileInfo, error) {
	parts, err := descriptorParts(name)
	if err != nil {
		return nil, err
	}
	directory, err := openDirectoryAt(fs.root, parts, false)
	if err != nil {
		return nil, err
	}
	defer directory.Close()
	entries, err := directory.ReadDir(-1)
	if err != nil {
		return nil, err
	}
	infos := make([]os.FileInfo, 0, len(entries))
	for _, entry := range entries {
		info, err := entry.Info()
		if err != nil {
			return nil, err
		}
		infos = append(infos, info)
	}
	return infos, nil
}

func (fs *descriptorBasic) MkdirAll(name string, _ os.FileMode) error {
	parts, err := descriptorParts(name)
	if err != nil {
		return err
	}
	directory, err := openDirectoryAt(fs.root, parts, true)
	if err != nil {
		return err
	}
	return directory.Close()
}

func (fs *descriptorBasic) TempFile(directory, prefix string) (billy.File, error) {
	parts, err := descriptorParts(directory)
	if err != nil {
		return nil, err
	}
	parent, err := openDirectoryAt(fs.root, parts, true)
	if err != nil {
		return nil, err
	}
	defer parent.Close()
	for attempt := 0; attempt < 100; attempt++ {
		name, err := randomEntryName(prefix)
		if err != nil {
			return nil, err
		}
		file, err := openRegularAt(parent, name, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0o600)
		if err == nil {
			return &descriptorFile{file}, nil
		}
		if !errors.Is(err, syscall.EEXIST) {
			return nil, err
		}
	}
	return nil, fmt.Errorf("create descriptor temporary file")
}

func (*descriptorBasic) Symlink(string, string) error { return fmt.Errorf("symlinks are forbidden") }
func (*descriptorBasic) Readlink(string) (string, error) {
	return "", fmt.Errorf("symlinks are forbidden")
}
func (*descriptorBasic) Capabilities() billy.Capability { return billy.AllCapabilities }
func (file *descriptorFile) Lock() error                { return unix.Flock(int(file.Fd()), unix.LOCK_EX) }
func (file *descriptorFile) Unlock() error              { return unix.Flock(int(file.Fd()), unix.LOCK_UN) }

var _ billy.Basic = (*descriptorBasic)(nil)
var _ billy.Dir = (*descriptorBasic)(nil)
var _ billy.TempFile = (*descriptorBasic)(nil)
var _ billy.Symlink = (*descriptorBasic)(nil)
