package snapshots

import (
	"context"
	gofs "io/fs"
	"os"
	"path/filepath"
	"strings"
	"syscall"

	"github.com/containerd/containerd/v2/core/leases"
	"github.com/containerd/containerd/v2/core/mount"
	"github.com/containerd/containerd/v2/core/snapshots"
	"github.com/containerd/containerd/v2/plugins/snapshots/overlay/overlayutils"
	continuityfs "github.com/containerd/continuity/fs"
	"github.com/containerd/continuity/sysx"
	"github.com/dagger/dagger/engine/snapshots/fsdiff"
	"github.com/dagger/dagger/internal/buildkit/identity"
	"github.com/dagger/dagger/internal/buildkit/util/bklog"
	"github.com/hashicorp/go-multierror"
	"github.com/pkg/errors"
	"golang.org/x/sys/unix"
)

// diffApply applies the provided diffs to the dest MountableRef and returns the correctly calculated disk usage
// that accounts for any hardlinks made from existing snapshots. ctx is expected to have a temporary lease
// associated with it.
func (sn *mergeSnapshotter) diffApply(ctx context.Context, dest MountableRef, diffs ...Diff) (_ snapshots.Usage, rerr error) {
	a, err := applierFor(dest, sn.tryCrossSnapshotLink, sn.userxattr)
	if err != nil {
		return snapshots.Usage{}, errors.Wrapf(err, "failed to create applier")
	}
	defer func() {
		releaseErr := a.Release()
		if releaseErr != nil {
			rerr = multierror.Append(rerr, errors.Wrapf(releaseErr, "failed to release applier")).ErrorOrNil()
		}
	}()

	for _, diff := range diffs {
		var lowerMntable MountableRef
		if diff.Lower != "" {
			if info, err := sn.Stat(ctx, diff.Lower); err != nil {
				return snapshots.Usage{}, errors.Wrapf(err, "failed to stat lower snapshot %s", diff.Lower)
			} else if info.Kind == snapshots.KindCommitted {
				lowerMntable, err = sn.View(ctx, identity.NewID(), diff.Lower)
				if err != nil {
					return snapshots.Usage{}, errors.Wrapf(err, "failed to mount lower snapshot view %s", diff.Lower)
				}
			} else {
				lowerMntable, err = sn.Mounts(ctx, diff.Lower)
				if err != nil {
					return snapshots.Usage{}, errors.Wrapf(err, "failed to mount lower snapshot %s", diff.Lower)
				}
			}
		}
		var upperMntable MountableRef
		if diff.Upper != "" {
			if info, err := sn.Stat(ctx, diff.Upper); err != nil {
				return snapshots.Usage{}, errors.Wrapf(err, "failed to stat upper snapshot %s", diff.Upper)
			} else if info.Kind == snapshots.KindCommitted {
				upperMntable, err = sn.View(ctx, identity.NewID(), diff.Upper)
				if err != nil {
					return snapshots.Usage{}, errors.Wrapf(err, "failed to mount upper snapshot view %s", diff.Upper)
				}
			} else {
				upperMntable, err = sn.Mounts(ctx, diff.Upper)
				if err != nil {
					return snapshots.Usage{}, errors.Wrapf(err, "failed to mount upper snapshot %s", diff.Upper)
				}
			}
		} else {
			upperMntable, err = sn.View(ctx, identity.NewID(), "")
			if err != nil {
				return snapshots.Usage{}, errors.Wrapf(err, "failed to mount empty upper snapshot view %s", diff.Upper)
			}
		}
		d, err := differFor(lowerMntable, upperMntable, diff.Comparison)
		if err != nil {
			return snapshots.Usage{}, errors.Wrapf(err, "failed to create differ")
		}
		defer func() {
			rerr = multierror.Append(rerr, d.Release()).ErrorOrNil()
		}()
		if err := d.HandleChanges(ctx, a.Apply); err != nil {
			return snapshots.Usage{}, errors.Wrapf(err, "failed to handle changes")
		}
	}

	if err := a.Flush(); err != nil {
		return snapshots.Usage{}, errors.Wrapf(err, "failed to flush changes")
	}
	return a.Usage()
}

type change struct {
	kind        continuityfs.ChangeKind
	subPath     string
	srcPath     string
	srcStat     *syscall.Stat_t
	linkSubPath string
}

type changeApply struct {
	*change
	dstPath   string
	dstStat   *syscall.Stat_t
	setOpaque bool
}

type inode struct {
	ino uint64
	dev uint64
}

func statInode(stat *syscall.Stat_t) inode {
	if stat == nil {
		return inode{}
	}
	return inode{
		ino: stat.Ino,
		dev: stat.Dev,
	}
}

type applier struct {
	root                 string
	release              func() error
	lowerdirs            []string
	crossSnapshotLinks   map[inode]struct{}
	createWhiteoutDelete bool
	userxattr            bool
	dirModTimes          map[string]unix.Timespec
}

func applierFor(dest MountableRef, tryCrossSnapshotLink, userxattr bool) (_ *applier, rerr error) {
	a := &applier{
		dirModTimes: make(map[string]unix.Timespec),
		userxattr:   userxattr,
	}
	defer func() {
		if rerr != nil {
			rerr = multierror.Append(rerr, a.Release()).ErrorOrNil()
		}
	}()
	if tryCrossSnapshotLink {
		a.crossSnapshotLinks = make(map[inode]struct{})
	}

	mnts, release, err := dest.Mount()
	if err != nil {
		return nil, nil
	}
	a.release = release

	if len(mnts) != 1 {
		return nil, errors.Errorf("expected exactly one mount, got %d", len(mnts))
	}
	mnt := mnts[0]

	if fsdiff.IsOverlayMountType(mnt) {
		for _, opt := range mnt.Options {
			if strings.HasPrefix(opt, "upperdir=") {
				a.root = strings.TrimPrefix(opt, "upperdir=")
			} else if strings.HasPrefix(opt, "lowerdir=") {
				a.lowerdirs = strings.Split(strings.TrimPrefix(opt, "lowerdir="), ":")
			}
		}
		if a.root == "" {
			return nil, errors.Errorf("could not find upperdir in mount options %v", mnt.Options)
		}
		if len(a.lowerdirs) == 0 {
			return nil, errors.Errorf("could not find lowerdir in mount options %v", mnt.Options)
		}
		a.createWhiteoutDelete = true
	} else if mnt.Type == "bind" || mnt.Type == "rbind" {
		a.root = mnt.Source
	} else {
		mnter := LocalMounter(dest)
		root, err := mnter.Mount()
		if err != nil {
			return nil, err
		}
		a.root = root
		prevRelease := a.release
		a.release = func() error {
			err := mnter.Unmount()
			return multierror.Append(err, prevRelease()).ErrorOrNil()
		}
	}

	a.root, err = filepath.EvalSymlinks(a.root)
	if err != nil {
		return nil, errors.Wrapf(err, "failed to resolve symlinks in %s", a.root)
	}
	return a, nil
}

func (a *applier) Apply(ctx context.Context, c *change) error {
	if c == nil {
		return errors.New("nil change")
	}

	if c.kind == continuityfs.ChangeKindUnmodified {
		return nil
	}

	dstPath, err := safeJoin(a.root, c.subPath)
	if err != nil {
		return errors.Wrapf(err, "failed to join paths %q and %q", a.root, c.subPath)
	}
	var dstStat *syscall.Stat_t
	if dstfi, err := os.Lstat(dstPath); err == nil {
		stat, ok := dstfi.Sys().(*syscall.Stat_t)
		if !ok {
			return errors.Errorf("failed to get stat_t for %T", dstStat)
		}
		dstStat = stat
	} else if !os.IsNotExist(err) {
		return errors.Wrap(err, "failed to stat during copy apply")
	}

	ca := &changeApply{
		change:  c,
		dstPath: dstPath,
		dstStat: dstStat,
	}

	if done, err := a.applyDelete(ca); err != nil {
		return errors.Wrap(err, "failed to delete during apply")
	} else if done {
		return nil
	}

	if done, err := a.applyHardlink(ctx, ca); err != nil {
		return errors.Wrapf(err, "failed to hardlink during apply")
	} else if done {
		return nil
	}

	if err := a.applyCopy(ctx, ca); err != nil {
		return errors.Wrapf(err, "failed to copy during apply")
	}
	return nil
}

func (a *applier) applyDelete(ca *changeApply) (bool, error) {
	deleteOnly := ca.kind == continuityfs.ChangeKindDelete
	overwrite := !deleteOnly && ca.dstStat != nil && ca.srcStat.Mode&ca.dstStat.Mode&unix.S_IFMT != unix.S_IFDIR

	if !deleteOnly && !overwrite {
		return false, nil
	}

	if err := os.RemoveAll(ca.dstPath); err != nil {
		return false, errors.Wrap(err, "failed to remove during apply")
	}
	ca.dstStat = nil

	if overwrite && a.createWhiteoutDelete && ca.srcStat.Mode&unix.S_IFMT == unix.S_IFDIR {
		ca.setOpaque = true
	}

	if deleteOnly && a.createWhiteoutDelete {
		var foundLower bool
		for _, lowerdir := range a.lowerdirs {
			lowerPath, err := safeJoin(lowerdir, ca.subPath)
			if err != nil {
				return false, errors.Wrapf(err, "failed to join lowerdir %q and subPath %q", lowerdir, ca.subPath)
			}
			if _, err := os.Lstat(lowerPath); err == nil {
				foundLower = true
				break
			} else if !errors.Is(err, unix.ENOENT) && !errors.Is(err, unix.ENOTDIR) {
				return false, errors.Wrapf(err, "failed to stat lowerPath %q", lowerPath)
			}
		}
		if foundLower {
			ca.kind = continuityfs.ChangeKindAdd
			if ca.srcStat == nil {
				ca.srcStat = &syscall.Stat_t{
					Mode: syscall.S_IFCHR,
					Rdev: unix.Mkdev(0, 0),
				}
				ca.srcPath = ""
			}
			return false, nil
		}
	}

	return deleteOnly, nil
}

func (a *applier) applyHardlink(ctx context.Context, ca *changeApply) (bool, error) {
	switch ca.srcStat.Mode & unix.S_IFMT {
	case unix.S_IFDIR, unix.S_IFIFO, unix.S_IFSOCK:
		return false, nil

	default:
		var linkSrcPath string
		if ca.linkSubPath != "" {
			path, err := safeJoin(a.root, ca.linkSubPath)
			if err != nil {
				return false, errors.Errorf("failed to get hardlink source path: %v", err)
			}
			linkSrcPath = path
		} else if a.crossSnapshotLinks != nil {
			linkSrcPath = ca.srcPath
			a.crossSnapshotLinks[statInode(ca.srcStat)] = struct{}{}
		}
		if linkSrcPath == "" {
			return false, nil
		}

		if err := os.Link(linkSrcPath, ca.dstPath); errors.Is(err, unix.EXDEV) || errors.Is(err, unix.EMLINK) {
			bklog.G(ctx).WithError(err).WithField("srcPath", linkSrcPath).WithField("dstPath", ca.dstPath).Debug("hardlink failed")
			if a.crossSnapshotLinks != nil {
				delete(a.crossSnapshotLinks, statInode(ca.srcStat))
			}
			return false, nil
		} else if err != nil {
			return false, errors.Wrap(err, "failed to hardlink during apply")
		}

		return true, nil
	}
}

func (a *applier) applyCopy(ctx context.Context, ca *changeApply) error {
	switch ca.srcStat.Mode & unix.S_IFMT {
	case unix.S_IFREG:
		if err := continuityfs.CopyFile(ca.dstPath, ca.srcPath); err != nil {
			return errors.Wrapf(err, "failed to copy from %s to %s during apply", ca.srcPath, ca.dstPath)
		}
	case unix.S_IFDIR:
		if ca.dstStat == nil {
			if err := unix.Mkdir(ca.dstPath, ca.srcStat.Mode); err != nil {
				return errors.Wrapf(err, "failed to create applied dir at %q from %q", ca.dstPath, ca.srcPath)
			}
		}
	case unix.S_IFLNK:
		if target, err := os.Readlink(ca.srcPath); err != nil {
			return errors.Wrap(err, "failed to read symlink during apply")
		} else if err := os.Symlink(target, ca.dstPath); err != nil {
			return errors.Wrap(err, "failed to create symlink during apply")
		}
	case unix.S_IFBLK, unix.S_IFCHR, unix.S_IFIFO, unix.S_IFSOCK:
		if err := unix.Mknod(ca.dstPath, ca.srcStat.Mode, int(ca.srcStat.Rdev)); err != nil {
			return errors.Wrap(err, "failed to mknod during apply")
		}
	default:
		return errors.Errorf("unhandled file type %d during merge at path %q", ca.srcStat.Mode&unix.S_IFMT, ca.srcPath)
	}

	if err := os.Lchown(ca.dstPath, int(ca.srcStat.Uid), int(ca.srcStat.Gid)); err != nil {
		return errors.Wrap(err, "failed to chown during apply")
	}

	if ca.srcStat.Mode&unix.S_IFMT != unix.S_IFLNK {
		if err := unix.Chmod(ca.dstPath, ca.srcStat.Mode); err != nil {
			return errors.Wrapf(err, "failed to chmod path %q during apply", ca.dstPath)
		}
	}

	if ca.srcPath != "" {
		xattrs, err := sysx.LListxattr(ca.srcPath)
		if err != nil {
			return errors.Wrapf(err, "failed to list xattrs of src path %s", ca.srcPath)
		}
		for _, xattr := range xattrs {
			if isOpaqueXattr(xattr) {
				continue
			}
			xattrVal, err := sysx.LGetxattr(ca.srcPath, xattr)
			if err != nil {
				return errors.Wrapf(err, "failed to get xattr %s of src path %s", xattr, ca.srcPath)
			}
			if err := sysx.LSetxattr(ca.dstPath, xattr, xattrVal, 0); err != nil {
				bklog.G(ctx).Debugf("failed to set xattr %s of path %s during apply", xattr, ca.dstPath)
			}
		}
	}

	if ca.setOpaque {
		xattr := opaqueXattr(a.userxattr)
		if err := sysx.LSetxattr(ca.dstPath, xattr, []byte{'y'}, 0); err != nil {
			return errors.Wrapf(err, "failed to set opaque xattr %q of path %s", xattr, ca.dstPath)
		}
	}

	atimeSpec := unix.Timespec{Sec: ca.srcStat.Atim.Sec, Nsec: ca.srcStat.Atim.Nsec}
	mtimeSpec := unix.Timespec{Sec: ca.srcStat.Mtim.Sec, Nsec: ca.srcStat.Mtim.Nsec}
	if ca.srcStat.Mode&unix.S_IFMT != unix.S_IFDIR {
		if err := unix.UtimesNanoAt(unix.AT_FDCWD, ca.dstPath, []unix.Timespec{atimeSpec, mtimeSpec}, unix.AT_SYMLINK_NOFOLLOW); err != nil {
			return err
		}
	} else {
		a.dirModTimes[ca.dstPath] = mtimeSpec
	}

	return nil
}

func (a *applier) Flush() error {
	return filepath.WalkDir(a.root, func(path string, d gofs.DirEntry, prevErr error) error {
		if prevErr != nil {
			return prevErr
		}
		if !d.IsDir() {
			return nil
		}
		if mtime, ok := a.dirModTimes[path]; ok {
			if err := unix.UtimesNanoAt(unix.AT_FDCWD, path, []unix.Timespec{{Nsec: unix.UTIME_OMIT}, mtime}, unix.AT_SYMLINK_NOFOLLOW); err != nil {
				return err
			}
		}
		return nil
	})
}

func (a *applier) Release() error {
	if a.release != nil {
		err := a.release()
		if err != nil {
			return err
		}
	}
	a.release = nil
	return nil
}

func (a *applier) Usage() (snapshots.Usage, error) {
	inodes := make(map[inode]struct{})
	var usage snapshots.Usage
	if err := filepath.WalkDir(a.root, func(path string, dirent gofs.DirEntry, err error) error {
		if err != nil {
			return err
		}
		info, err := dirent.Info()
		if err != nil {
			return err
		}
		stat := info.Sys().(*syscall.Stat_t)
		inode := statInode(stat)
		if _, ok := inodes[inode]; ok {
			return nil
		}
		inodes[inode] = struct{}{}
		if a.crossSnapshotLinks != nil {
			if _, ok := a.crossSnapshotLinks[statInode(stat)]; ok {
				return nil
			}
		}
		usage.Inodes++
		usage.Size += stat.Blocks * 512
		return nil
	}); err != nil {
		return snapshots.Usage{}, err
	}
	return usage, nil
}

type differ struct {
	lowerRoot    string
	releaseLower func() error

	upperRoot    string
	releaseUpper func() error

	upperBindSource  string
	upperOverlayDirs []string

	upperdir string

	visited    map[string]struct{}
	inodes     map[inode]string
	comparison fsdiff.Comparison
}

func differFor(lowerMntable, upperMntable MountableRef, comparison fsdiff.Comparison) (_ *differ, rerr error) {
	d := &differ{
		visited:    make(map[string]struct{}),
		inodes:     make(map[inode]string),
		comparison: comparison,
	}
	defer func() {
		if rerr != nil {
			rerr = multierror.Append(rerr, d.Release()).ErrorOrNil()
		}
	}()

	var lowerMnts []mount.Mount
	if lowerMntable != nil {
		mnts, release, err := lowerMntable.Mount()
		if err != nil {
			return nil, err
		}
		mounter := LocalMounterWithMounts(mnts)
		root, err := mounter.Mount()
		if err != nil {
			return nil, err
		}
		d.lowerRoot = root
		lowerMnts = mnts
		d.releaseLower = func() error {
			err := mounter.Unmount()
			return multierror.Append(err, release()).ErrorOrNil()
		}
	}

	var upperMnts []mount.Mount
	if upperMntable != nil {
		mnts, release, err := upperMntable.Mount()
		if err != nil {
			return nil, err
		}
		mounter := LocalMounterWithMounts(mnts)
		root, err := mounter.Mount()
		if err != nil {
			return nil, err
		}
		d.upperRoot = root
		upperMnts = mnts
		d.releaseUpper = func() error {
			err := mounter.Unmount()
			return multierror.Append(err, release()).ErrorOrNil()
		}
	}

	if len(upperMnts) == 1 {
		if upperMnts[0].Type == "bind" || upperMnts[0].Type == "rbind" {
			d.upperBindSource = upperMnts[0].Source
		} else if fsdiff.IsOverlayMountType(upperMnts[0]) {
			overlayDirs, err := fsdiff.GetOverlayLayers(upperMnts[0])
			if err != nil {
				return nil, errors.Wrapf(err, "failed to get overlay layers from mount %+v", upperMnts[0])
			}
			d.upperOverlayDirs = overlayDirs
		}
	}
	if len(lowerMnts) > 0 {
		if upperdir, err := fsdiff.GetUpperdir(lowerMnts, upperMnts); err == nil {
			d.upperdir = upperdir
		}
	}

	return d, nil
}

func (d *differ) HandleChanges(ctx context.Context, handle func(context.Context, *change) error) error {
	if d.upperdir != "" {
		return d.overlayChanges(ctx, handle)
	}
	return d.doubleWalkingChanges(ctx, handle)
}

func (d *differ) doubleWalkingChanges(ctx context.Context, handle func(context.Context, *change) error) error {
	return fsdiff.WalkChanges(ctx, d.lowerRoot, d.upperRoot, d.comparison, func(kind continuityfs.ChangeKind, subPath string, srcfi os.FileInfo, prevErr error) error {
		if prevErr != nil {
			return prevErr
		}
		select {
		case <-ctx.Done():
			return context.Cause(ctx)
		default:
		}

		if kind == continuityfs.ChangeKindUnmodified {
			return nil
		}

		if err := d.checkParent(ctx, subPath, handle); err != nil {
			return errors.Wrapf(err, "failed to check parent for %s", subPath)
		}

		c := &change{
			kind:    kind,
			subPath: subPath,
		}

		if srcfi != nil {
			switch {
			case !srcfi.IsDir() && d.upperBindSource != "":
				srcPath, err := safeJoin(d.upperBindSource, c.subPath)
				if err != nil {
					return errors.Wrapf(err, "failed to join %s and %s", d.upperBindSource, c.subPath)
				}
				c.srcPath = srcPath
				if fi, err := os.Lstat(c.srcPath); err == nil {
					srcfi = fi
				} else {
					return errors.Wrap(err, "failed to stat underlying file from bind mount")
				}
			case !srcfi.IsDir() && len(d.upperOverlayDirs) > 0:
				for i := range d.upperOverlayDirs {
					dir := d.upperOverlayDirs[len(d.upperOverlayDirs)-1-i]
					path, err := safeJoin(dir, c.subPath)
					if err != nil {
						return errors.Wrapf(err, "failed to join %s and %s", dir, c.subPath)
					}
					if stat, err := os.Lstat(path); err == nil {
						c.srcPath = path
						srcfi = stat
						break
					} else if errors.Is(err, unix.ENOENT) {
						continue
					} else {
						return errors.Wrap(err, "failed to lstat when finding direct path of overlay file")
					}
				}
			default:
				srcPath, err := safeJoin(d.upperRoot, subPath)
				if err != nil {
					return errors.Wrapf(err, "failed to join %s and %s", d.upperRoot, subPath)
				}
				c.srcPath = srcPath
				if fi, err := os.Lstat(c.srcPath); err == nil {
					srcfi = fi
				} else {
					return errors.Wrap(err, "failed to stat srcPath from differ")
				}
			}

			var ok bool
			c.srcStat, ok = srcfi.Sys().(*syscall.Stat_t)
			if !ok {
				return errors.Errorf("unhandled stat type for %+v", srcfi)
			}

			if !srcfi.IsDir() && c.srcStat.Nlink > 1 {
				if linkSubPath, ok := d.inodes[statInode(c.srcStat)]; ok {
					c.linkSubPath = linkSubPath
				} else {
					d.inodes[statInode(c.srcStat)] = c.subPath
				}
			}
		}

		return handle(ctx, c)
	})
}

func (d *differ) overlayChanges(ctx context.Context, handle func(context.Context, *change) error) error {
	return fsdiff.WalkUpperdirChanges(ctx, func(kind continuityfs.ChangeKind, subPath string, srcfi os.FileInfo, prevErr error) error {
		if prevErr != nil {
			return prevErr
		}

		select {
		case <-ctx.Done():
			return context.Cause(ctx)
		default:
		}

		if kind == continuityfs.ChangeKindUnmodified {
			return nil
		}

		if err := d.checkParent(ctx, subPath, handle); err != nil {
			return errors.Wrapf(err, "failed to check parent for %s", subPath)
		}

		srcPath, err := safeJoin(d.upperdir, subPath)
		if err != nil {
			return errors.Wrapf(err, "failed to join %s and %s", d.upperdir, subPath)
		}

		c := &change{
			kind:    kind,
			subPath: subPath,
			srcPath: srcPath,
		}

		if srcfi != nil {
			var ok bool
			c.srcStat, ok = srcfi.Sys().(*syscall.Stat_t)
			if !ok {
				return errors.Errorf("unhandled stat type for %+v", srcfi)
			}

			if !srcfi.IsDir() && c.srcStat.Nlink > 1 && c.kind != continuityfs.ChangeKindDelete {
				if linkSubPath, ok := d.inodes[statInode(c.srcStat)]; ok {
					c.linkSubPath = linkSubPath
				} else {
					d.inodes[statInode(c.srcStat)] = c.subPath
				}
			}
		}

		return handle(ctx, c)
	}, d.upperdir, d.upperRoot, d.lowerRoot, d.comparison)
}

func (d *differ) checkParent(ctx context.Context, subPath string, handle func(context.Context, *change) error) error {
	parentSubPath := filepath.Dir(subPath)
	if parentSubPath == "/" {
		return nil
	}
	if _, ok := d.visited[parentSubPath]; ok {
		return nil
	}
	d.visited[parentSubPath] = struct{}{}

	if err := d.checkParent(ctx, parentSubPath, handle); err != nil {
		return err
	}
	parentSrcPath, err := safeJoin(d.upperRoot, parentSubPath)
	if err != nil {
		return err
	}
	srcfi, err := os.Lstat(parentSrcPath)
	if err != nil {
		return err
	}
	parentSrcStat, ok := srcfi.Sys().(*syscall.Stat_t)
	if !ok {
		return errors.Errorf("unexpected type %T", srcfi)
	}
	return handle(ctx, &change{
		kind:    continuityfs.ChangeKindModify,
		subPath: parentSubPath,
		srcPath: parentSrcPath,
		srcStat: parentSrcStat,
	})
}

func (d *differ) Release() error {
	var err error
	if d.releaseLower != nil {
		err = d.releaseLower()
		if err == nil {
			d.releaseLower = nil
		}
	}
	if d.releaseUpper != nil {
		err = multierror.Append(err, d.releaseUpper()).ErrorOrNil()
		if err == nil {
			d.releaseUpper = nil
		}
	}
	return err
}

func safeJoin(root, path string) (string, error) {
	dir, base := filepath.Split(path)
	parent, err := continuityfs.RootPath(root, dir)
	if err != nil {
		return "", err
	}
	return filepath.Join(parent, base), nil
}

const (
	trustedOpaqueXattr = "trusted.overlay.opaque"
	userOpaqueXattr    = "user.overlay.opaque"
)

func isOpaqueXattr(s string) bool {
	for _, k := range []string{trustedOpaqueXattr, userOpaqueXattr} {
		if s == k {
			return true
		}
	}
	return false
}

func opaqueXattr(userxattr bool) string {
	if userxattr {
		return userOpaqueXattr
	}
	return trustedOpaqueXattr
}

// needsUserXAttr checks whether overlay mounts should be provided the userxattr option. We can't use
// NeedsUserXAttr from the overlayutils package directly because we don't always have direct knowledge
// of the root of the snapshotter state (such as when using a remote snapshotter). Instead, we create
// a temporary new snapshot and test using its root, which works because single layer snapshots will
// use bind-mounts even when created by an overlay based snapshotter.
func needsUserXAttr(ctx context.Context, sn Snapshotter, lm leases.Manager) (bool, error) {
	key := identity.NewID()

	if leaseID, ok := leases.FromContext(ctx); !ok || leaseID == "" {
		leaseCtx, err := EnsureLease(ctx)
		if err != nil {
			return false, errors.Wrap(err, "failed to ensure lease for checking user xattr")
		}
		ctx = leaseCtx
	}
	if leaseID, ok := leases.FromContext(ctx); !ok || leaseID == "" {
		leaseCtx, done, err := WithLease(ctx, lm, MakeTemporary)
		if err != nil {
			return false, errors.Wrap(err, "failed to create lease for checking user xattr")
		}
		defer done(context.TODO())
		ctx = leaseCtx
	}

	err := sn.Prepare(ctx, key, "")
	if err != nil {
		return false, err
	}
	mntable, err := sn.Mounts(ctx, key)
	if err != nil {
		return false, err
	}
	mnts, unmount, err := mntable.Mount()
	if err != nil {
		return false, err
	}
	defer unmount()

	var userxattr bool
	if err := mount.WithTempMount(ctx, mnts, func(root string) error {
		var err error
		userxattr, err = overlayutils.NeedsUserXAttr(root)
		return err
	}); err != nil {
		return false, err
	}
	return userxattr, nil
}
