package utils

import (
	"container/list"
	"context"
	"errors"
	"fmt"
	"io"
	"io/fs"
	"os"
	"path/filepath"
	"sync/atomic"
	"syscall"
	"time"

	"github.com/kovidgoyal/go-parallel"
	"golang.org/x/sys/unix"
)

var _ = fmt.Print

// MkdirAt creates a new subdirectory named 'name' inside the directory
// pointed to by parentDir.
func MkdirAt(parentDir *os.File, name string, perm os.FileMode) (err error) {
	// parentDir.Fd() gives us the base directory handle
	fd := int(parentDir.Fd())

	// unix.Mkdirat(dirfd, path, mode)
	// We convert the os.FileMode to a uint32 for the syscall
	for {
		if err = unix.Mkdirat(fd, name, uint32(perm)); err != unix.EINTR {
			break
		}
	}
	if err != nil {
		return &fs.PathError{
			Op:   "mkdirat",
			Path: filepath.Join(parentDir.Name(), name),
			Err:  err,
		}
	}
	return nil
}

// OpenAt opens a file relative to the directory pointed to by dirFile.
// Matches the behavior of os.Open (read-only).
func OpenAt(dirFile *os.File, name string) (*os.File, error) {
	return openAt(dirFile, name, unix.O_RDONLY, 0)
}

// Opens a directory relative to the directory pointed to by dirFile.
// Matches the behavior of os.Open (read-only).
func OpenDirAt(dirFile *os.File, name string) (*os.File, error) {
	return openAt(dirFile, name, unix.O_RDONLY|unix.O_DIRECTORY, 0)
}

// Create a symlink named name in the directory pointed to by dirFile. The
// target of the symlink is set to target
func SymlinkAt(dirFile *os.File, name, target string) (err error) {
	for {
		if err = unix.Symlinkat(target, int(dirFile.Fd()), name); err != unix.EINTR {
			break
		}
	}
	if err != nil {
		return &fs.PathError{
			Op:   "symlinkat",
			Path: filepath.Join(dirFile.Name(), name),
			Err:  err,
		}

	}
	return
}

// CreateAt creates or truncates a file relative to the directory pointed to by dirFile.
// Matches the behavior of os.Create (read-write, creates if doesn't exist, truncates).
func CreateAt(dirFile *os.File, name string, permissions os.FileMode) (*os.File, error) {
	return openAt(dirFile, name, unix.O_RDWR|unix.O_CREAT|unix.O_TRUNC, permissions)
}

// Create the specified directory, open it and return the file object. If the
// directory already exists, it is opened and returned, without changing its
// permissions, matching the behavior of CreateAt().
func CreateDirAt(parent *os.File, name string, permissions os.FileMode) (*os.File, error) {
	if err := MkdirAt(parent, name, permissions); err != nil {
		if errors.Is(err, unix.EEXIST) {
			return OpenDirAt(parent, name)
		}
		return nil, err
	}
	return OpenDirAt(parent, name)
}

// Internal helper to wrap the unix.Openat syscall
func openAt(dirFile *os.File, name string, flags int, perm os.FileMode) (ans *os.File, err error) {
	dirFd := int(dirFile.Fd())
	// Call the underlying system call
	var fd int
	for {
		if fd, err = unix.Openat(dirFd, name, flags|unix.O_CLOEXEC, uint32(perm)); err != unix.EINTR {
			break
		}
	}
	name = filepath.Join(dirFile.Name(), name)
	if err != nil {
		return nil, &os.PathError{Op: "openat", Path: name, Err: err}
	}
	return os.NewFile(uintptr(fd), name), nil
}

type UnixFileInfo struct {
	name string
	stat *unix.Stat_t
	mode os.FileMode
}

func NewUnixFileInfo(name string, stat *unix.Stat_t) os.FileInfo {
	rawMode := stat.Mode
	// Start with the standard 9-bit permissions
	mode := os.FileMode(rawMode & 0777)

	// Map the file type bits using S_IFMT
	switch rawMode & unix.S_IFMT {
	case unix.S_IFDIR:
		mode |= os.ModeDir
	case unix.S_IFLNK:
		mode |= os.ModeSymlink
	case unix.S_IFBLK:
		mode |= os.ModeDevice
	case unix.S_IFCHR:
		// Go uses ModeDevice | ModeCharDevice for character devices
		mode |= os.ModeDevice | os.ModeCharDevice
	case unix.S_IFIFO:
		mode |= os.ModeNamedPipe
	case unix.S_IFSOCK:
		mode |= os.ModeSocket
	}

	// Map setuid, setgid, and sticky bits
	if rawMode&unix.S_ISUID != 0 {
		mode |= os.ModeSetuid
	}
	if rawMode&unix.S_ISGID != 0 {
		mode |= os.ModeSetgid
	}
	if rawMode&unix.S_ISVTX != 0 {
		mode |= os.ModeSticky
	}
	return &UnixFileInfo{name, stat, mode}
}

func (m *UnixFileInfo) Name() string       { return m.name }
func (m *UnixFileInfo) Size() int64        { return m.stat.Size }
func (m *UnixFileInfo) Mode() os.FileMode  { return m.mode }
func (m *UnixFileInfo) ModTime() time.Time { return time.Unix(m.stat.Mtim.Unix()) }
func (m *UnixFileInfo) IsDir() bool        { return m.Mode().IsDir() }
func (m *UnixFileInfo) Sys() any           { return m.stat }
func (m *UnixFileInfo) Dev() uint64        { return uint64(m.stat.Rdev) }

// Get file info relative to the parent FD, follows symlinks
func StatAt(dirFile *os.File, name string) (ans os.FileInfo, err error) {
	var stat unix.Stat_t
	for {
		if err = unix.Fstatat(int(dirFile.Fd()), name, &stat, 0); err != unix.EINTR {
			break
		}
	}
	if err != nil {
		name = filepath.Join(dirFile.Name(), name)
		return nil, &os.PathError{Op: "statat", Path: name, Err: err}
	}
	return NewUnixFileInfo(name, &stat), nil
}

// Get file info relative to the parent FD, do not follows symlinks
func LstatAt(dirFile *os.File, name string) (ans os.FileInfo, err error) {
	var stat unix.Stat_t
	for {
		if err = unix.Fstatat(int(dirFile.Fd()), name, &stat, unix.AT_SYMLINK_NOFOLLOW); err != unix.EINTR {
			break
		}
	}
	if err != nil {
		name = filepath.Join(dirFile.Name(), name)
		return nil, &os.PathError{Op: "lstatat", Path: name, Err: err}
	}
	return NewUnixFileInfo(name, &stat), nil
}

// Remove file relative to parent fd
func UnlinkAt(parent *os.File, name string) (err error) {
	for {
		if err = unix.Unlinkat(int(parent.Fd()), name, 0); err != unix.EINTR {
			break
		}
	}
	if err != nil {
		err = &os.PathError{Op: "unlinkat", Path: filepath.Join(parent.Name(), name), Err: err}
	}
	return
}

// Remove empty directory relative to parent fd
func RemoveDirAt(parent *os.File, name string) (err error) {
	for {
		if err = unix.Unlinkat(int(parent.Fd()), name, unix.AT_REMOVEDIR); err != unix.EINTR {
			break
		}
	}
	if err != nil {
		err = &os.PathError{Op: "unlinkat", Path: filepath.Join(parent.Name(), name), Err: err}
	}
	return
}

// Create a hardlink pointing to oldname called newname relative to the
// specified directories. If oldname is a symlink,
// a new symlink is created pointing to its target when follow_symlinks is true otherwise to it.
func LinkAt(oldparent *os.File, oldname string, newparent *os.File, newname string, follow_symlinks bool) (err error) {
	flags := IfElse(follow_symlinks, unix.AT_SYMLINK_FOLLOW, 0)
	for {
		if err = unix.Linkat(int(oldparent.Fd()), oldname, int(newparent.Fd()), newname, flags); err != unix.EINTR {
			break
		}
	}
	if err != nil {
		err = &os.PathError{Op: "linkat", Path: fmt.Sprintf("%s -> %s", filepath.Join(newparent.Name(), newname), filepath.Join(oldparent.Name(), oldname)), Err: err}
	}
	return
}

// RemoveChildren removes all files and subdirectories within the directory
// pointed to by dirFile using an explicit stack instead of recursion. Removes
// all it can but returns the first error, if any.
func RemoveChildren(dirFile *os.File) error {
	var firstErr error

	// Each stack frame is one of two kinds:
	//   expand: dir != nil  – read dir's children, unlink files, push subdirs
	//   rmdir:  dir == nil  – rmdir name from parent, then Unref parent
	type frame struct {
		dir    *RefCountedFile // non-nil: expand this directory (Unref when done)
		name   string          // rmdir sentinel: child name to remove from parent
		parent *RefCountedFile // rmdir sentinel: ref to parent dir (Unref after rmdir)
	}

	// Only the root directory needs rewinding; it was passed in from outside
	// and may have been read before. Child dirs are freshly opened by us.
	if _, err := dirFile.Seek(0, io.SeekStart); err != nil {
		return err
	}
	rcRoot := NewRefCountedFile(dirFile)
	// Extra ref so the expand frame's Unref doesn't close the caller's fd.
	rcRoot.NewRef()

	stack := []frame{{dir: rcRoot}}

	for len(stack) > 0 {
		f := stack[len(stack)-1]
		stack = stack[:len(stack)-1]

		if f.dir == nil {
			// rmdir sentinel: the subdirectory is now empty, remove it.
			if err := RemoveDirAt(f.parent.File(), f.name); err != nil && firstErr == nil {
				firstErr = err
			}
			f.parent.Unref()
			continue
		}

		for {
			// Read entries in small chunks to handle very large directories.
			entries, err := f.dir.File().ReadDir(64)
			for _, entry := range entries {
				name := entry.Name()
				if entry.IsDir() {
					childFile, openErr := OpenDirAt(f.dir.File(), name)
					if openErr != nil {
						if firstErr == nil {
							firstErr = openErr
						}
						continue
					}
					// Push rmdir sentinel first; LIFO ensures the child expand
					// frame is processed before this rmdir sentinel.
					// f.dir.NewRef() adds a ref to the parent for the sentinel.
					stack = append(stack, frame{name: name, parent: f.dir.NewRef()}, frame{dir: NewRefCountedFile(childFile)})
				} else {
					if unlinkErr := UnlinkAt(f.dir.File(), name); unlinkErr != nil && firstErr == nil {
						firstErr = unlinkErr
					}
				}
			}
			if err != nil {
				if !errors.Is(err, io.EOF) && firstErr == nil {
					firstErr = &os.PathError{Op: "readdir", Path: f.dir.File().Name(), Err: err}
				}
				break
			}
		}

		f.dir.Unref()
	}

	_, _ = dirFile.Seek(0, io.SeekStart)
	return firstErr
}

func DupFile(f *os.File) (ans *os.File, err error) {
	var fd int
	for {
		if fd, err = unix.Dup(int(f.Fd())); err != unix.EINTR {
			break
		}
	}
	if err != nil {
		return nil, &os.PathError{Op: "dup", Path: f.Name(), Err: err}
	}
	return os.NewFile(uintptr(fd), f.Name()), nil
}

func ReadLinkAt(parent *os.File, name string) (ans string, err error) {
	buf := [unix.PathMax]byte{}
	n, err := readLinkAt(parent, name, buf[:])
	if err != nil {
		return "", &os.PathError{Op: "readlinkat", Path: filepath.Join(parent.Name(), name), Err: err}
	}
	return UnsafeBytesToString(buf[:n]), nil
}

func RenameAt(old_parent *os.File, old_name string, new_parent *os.File, new_name string) (err error) {
	for {
		if err = unix.Renameat(int(old_parent.Fd()), old_name, int(new_parent.Fd()), new_name); err != unix.EINTR {
			break
		}
	}
	if err != nil {
		err = &os.LinkError{Op: "renameat", Old: filepath.Join(old_parent.Name(), old_name), New: filepath.Join(new_parent.Name(), new_name), Err: err}
	}
	return
}

func ConvertFileModeToUnix(goMode os.FileMode) uint32 {
	// 1. Start with the basic permission bits (0777)
	unixMode := uint32(goMode.Perm())

	// 2. Map the type bits
	// We use the os.ModeXXX constants to identify the type
	switch {
	case goMode.IsDir():
		unixMode |= unix.S_IFDIR
	case goMode&os.ModeSymlink != 0:
		unixMode |= unix.S_IFLNK
	case goMode&os.ModeNamedPipe != 0:
		unixMode |= unix.S_IFIFO
	case goMode&os.ModeSocket != 0:
		unixMode |= unix.S_IFSOCK
	case goMode&os.ModeDevice != 0:
		if goMode&os.ModeCharDevice != 0 {
			unixMode |= unix.S_IFCHR
		} else {
			unixMode |= unix.S_IFBLK
		}
	default:
		// Default to a regular file
		unixMode |= unix.S_IFREG
	}

	// 3. Map special bits
	if goMode&os.ModeSetuid != 0 {
		unixMode |= unix.S_ISUID
	}
	if goMode&os.ModeSetgid != 0 {
		unixMode |= unix.S_ISGID
	}
	if goMode&os.ModeSticky != 0 {
		unixMode |= unix.S_ISVTX
	}

	return unixMode
}

func MknodAt(parent *os.File, name string, mode os.FileMode, dev uint64) (err error) {
	unix_mode := ConvertFileModeToUnix(mode)
	if err = mknodAt(parent, name, unix_mode, dev); err != nil {
		err = &os.PathError{Op: "mknodat", Path: filepath.Join(parent.Name(), name), Err: err}
	}
	return
}

// Not thread safe reference counted wrapper for os.File
type RefCountedFile struct {
	f      *os.File
	refcnt atomic.Int32
}

func NewRefCountedFile(f *os.File) *RefCountedFile {
	ans := RefCountedFile{f: f}
	ans.refcnt.Add(1)
	return &ans
}

func (f *RefCountedFile) NewRef() *RefCountedFile {
	f.refcnt.Add(1)
	return f
}

func (f *RefCountedFile) Unref() *RefCountedFile {
	if f.refcnt.Add(-1) == 0 {
		f.f.Close()
		f.f = nil
	}
	return nil
}

func (f *RefCountedFile) File() *os.File { return f.f }

type CopyFolderOptions struct {
	Disallow_hardlinks bool
	Follow_symlinks    bool
	Filter_files       func(parent *os.File, child os.FileInfo) bool
}

// Copy the file objects as efficiently as possible with cancellation. The
// files are always closed before this function returns.
func CopyFileAndClose(ctx context.Context, src *os.File, dest *os.File) (err error) {
	err_chan := make(chan error)
	go func() {
		defer func() {
			if r := recover(); r != nil {
				err_chan <- parallel.Format_stacktrace_on_panic(r, 1)
			}
		}()
		// this go routine will automatically exit when src/dest are closed
		// even if copying is not complete. io.Copy() automatically use
		// sendfile() or similar mechanisms for efficiency.
		_, err := io.Copy(dest, src)
		err_chan <- err
	}()

	select {
	case <-ctx.Done():
		src.Close()
		dest.Close()
		// wait for go routine to exit
		<-err_chan
		return ctx.Err()
	case err := <-err_chan:
		src.Close()
		dest.Close()
		return err
	}
}

// Copy the contents of src_folder to dest_folder, recursively, preserving file
// permissions. Behavior around hard and symbolic links and filtering is
// controlled via the provided options. When symlink following is enabled,
// symlink loops are avoided and any sumlink that points to an already copied
// entry becomes a symlink pointing to the copied entry using a relative path.
// Existing regular files are overwritten, without changing their permissions.
// Existing directories also do not have their permissions updated.
func CopyFolderContents(ctx context.Context, src_folder *os.File, dest_folder *os.File, opts CopyFolderOptions) (final_error error) {
	// Ensure we get all dir contents
	_, err := src_folder.Seek(0, io.SeekStart)
	if err != nil {
		return err
	}
	// When following symlinks, store previously seen source items with the
	// abspaths they have been copied to in dest. Items are identified with
	// device + inode number which should be globally unique. This ensures 1)
	// no file/dir is copied more than once because of a symlink 2) symlinks
	// are changed to point to the relative location of the previously copied
	// target
	type dir_ident struct{ dev, inode uint64 }
	get_dir_ident := func(i os.FileInfo) dir_ident {
		switch s := i.Sys().(type) {
		case *syscall.Stat_t:
			return dir_ident{uint64(s.Dev), uint64(s.Ino)}
		case *unix.Stat_t:
			return dir_ident{uint64(s.Dev), uint64(s.Ino)}
		}
		panic("unknown stat result type from os.FileInfo")
	}
	var seen_map map[dir_ident]string
	if opts.Follow_symlinks {
		seen_map = make(map[dir_ident]string)
		if s, err := src_folder.Stat(); err != nil {
			return err
		} else {
			seen_map[get_dir_ident(s)] = dest_folder.Name()
		}
	}

	is_ok := opts.Filter_files
	if is_ok == nil {
		is_ok = func(*os.File, os.FileInfo) bool { return true }
	}
	type item struct {
		src_parent, dest_parent *RefCountedFile
		child                   os.FileInfo
	}
	queue := list.New()
	is_cancelled := func() bool {
		select {
		case <-ctx.Done():
			final_error = ctx.Err()
			return true
		default:
			return false
		}
	}
	defer func() {
		for {
			v := queue.Front()
			if v == nil {
				break
			}
			item := queue.Remove(v).(*item)
			if item.src_parent != nil {
				item.src_parent.Unref()
			}
			if item.dest_parent != nil {
				item.dest_parent.Unref()
			}
		}
	}()

	src, dest := NewRefCountedFile(src_folder), NewRefCountedFile(dest_folder)
	// Add an extra reference so that the files passed into this function are
	// not closed in do_one()
	src.NewRef()
	dest.NewRef()
	fail := func(lerr error) bool {
		final_error = lerr
		return false
	}
	mark_as_seen := func(dest_parent *os.File, child os.FileInfo) {
		if opts.Follow_symlinks {
			path := filepath.Join(dest_parent.Name(), child.Name())
			seen_map[get_dir_ident(child)] = filepath.Clean(path)
		}
	}

	var do_one_child func(src *RefCountedFile, dest *RefCountedFile, child os.FileInfo, from_symlink bool) bool
	do_one_child = func(src *RefCountedFile, dest *RefCountedFile, child os.FileInfo, from_symlink bool) bool {
		if child.IsDir() {
			queue.PushBack(&item{src.NewRef(), dest.NewRef(), child})
			return true
		}
		mark_as_seen(dest.File(), child)
		// First try a hardlink which works for regular files and symlinks at least
		if !opts.Disallow_hardlinks && LinkAt(src.File(), child.Name(), dest.File(), child.Name(), opts.Follow_symlinks) == nil {
			return true
		}
		t := child.Mode().Type()
		switch {
		case t.IsRegular():
			sf, err := OpenAt(src.File(), child.Name())
			if err != nil {
				return fail(err)
			}
			df, err := CreateAt(dest.File(), child.Name(), child.Mode().Perm())
			if err != nil {
				sf.Close()
				return fail(err)
			}
			if err = CopyFileAndClose(ctx, sf, df); err != nil {
				UnlinkAt(dest.File(), child.Name()) // dont leave partially copied files around
				return fail(err)
			}
		case t&os.ModeSymlink != 0:
			if opts.Follow_symlinks && !from_symlink {
				rpath, err := filepath.EvalSymlinks(filepath.Join(src.File().Name(), child.Name()))
				if err != nil {
					return do_one_child(src, dest, child, true)
				}
				parent_dir := filepath.Dir(rpath)
				pfd, err := unix.Open(parent_dir, unix.O_DIRECTORY|unix.O_RDONLY|unix.O_CLOEXEC, 0)
				if err != nil {
					return do_one_child(src, dest, child, true)
				}
				child_name := filepath.Base(rpath)
				return func() bool {
					pdf := os.NewFile(uintptr(pfd), parent_dir)
					// Use a RefCountedFile so that if st is a directory the fd
					// stays alive until the queued item is processed by next_dir.
					rcf := NewRefCountedFile(pdf)
					defer rcf.Unref()
					st, err := StatAt(pdf, child_name)
					if err != nil {
						return do_one_child(src, dest, child, true)
					}
					id := get_dir_ident(st)
					if existing_path, found := seen_map[id]; found {
						target, err := filepath.Rel(dest.File().Name(), existing_path)
						if err != nil {
							return do_one_child(src, dest, child, true)
						}
						if err = SymlinkAt(dest.File(), child.Name(), target); err != nil {
							return fail(err)
						}
						return true
					}
					return do_one_child(rcf, dest, st, true)
				}()
			} else {
				target, err := ReadLinkAt(src.File(), child.Name())
				if err != nil {
					return fail(err)
				}
				if err = SymlinkAt(dest.File(), child.Name(), target); err != nil {
					return fail(err)
				}
			}
		case t&os.ModeDevice != 0:
			if err := MknodAt(dest.File(), child.Name(), child.Mode(), child.(*UnixFileInfo).Dev()); err != nil {
				return fail(err)
			}
		}
		return true
	}

	do_one := func(src *RefCountedFile, dest *RefCountedFile) bool {
		defer func() {
			src = src.Unref()
			dest = dest.Unref()
		}()
		if is_cancelled() {
			return false
		}
		for {
			dir_entries, err := src.File().ReadDir(64)
			if err != nil {
				if errors.Is(err, io.EOF) {
					break
				}
				return fail(err)
			}
			for _, entry := range dir_entries {
				if is_cancelled() {
					return false
				}
				child, err := entry.Info()
				if err != nil {
					return fail(err)
				}
				if !is_ok(src.File(), child) {
					continue
				}
				if !do_one_child(src, dest, child, false) {
					return false
				}
			}
		}
		return true
	}

	next_dir := func(src_parent *RefCountedFile, dest_parent *RefCountedFile, child os.FileInfo) (ok bool) {
		src, dest = nil, nil
		mark_as_seen(dest_parent.File(), child)
		defer func() {
			src_parent.Unref()
			dest_parent.Unref()
			if !ok {
				if src != nil {
					src = src.Unref()
				}
				if dest != nil {
					dest = dest.Unref()
				}
			}
		}()
		sf, err := OpenDirAt(src_parent.File(), child.Name())
		if err != nil {
			final_error = err
			return false
		}
		df, err := CreateDirAt(dest_parent.File(), child.Name(), child.Mode().Perm())
		if err != nil {
			sf.Close()
			final_error = err
			return false
		}
		src, dest = NewRefCountedFile(sf), NewRefCountedFile(df)
		ok = true
		return
	}

	for {
		if !do_one(src, dest) {
			return
		}
		v := queue.Front()
		if v == nil {
			break
		}
		n := queue.Remove(v).(*item)
		if !next_dir(n.src_parent, n.dest_parent, n.child) {
			return
		}
	}
	return
}
