package archive

import (
	"archive/tar"
	"fmt"
	"os"
	"path"
	"path/filepath"
	"strings"

	"github.com/moby/go-archive/internal/archiveoptions"
	"github.com/moby/sys/userns"
	"golang.org/x/sys/unix"
)

func withProcSelfFD(opts *TarOptions) (*TarOptions, func(), error) {
	procSelfFD, err := os.Open("/proc/self/fd")
	if err != nil {
		return nil, nil, err
	}

	var prepared TarOptions
	if opts != nil {
		prepared = *opts
	}
	prepared.internalOptions = &archiveoptions.Options{
		ProcSelfFD: procSelfFD,
	}

	return &prepared, func() { _ = procSelfFD.Close() }, nil
}

func getWhiteoutConverter(format WhiteoutFormat) tarWhiteoutConverter {
	if format == OverlayWhiteoutFormat {
		return newOverlayWhiteoutConverter()
	}
	return nil
}

type overlayWhiteoutConverter struct {
	opaqueXattr string
}

func newOverlayWhiteoutConverter() overlayWhiteoutConverter {
	opaqueXattr := "trusted.overlay.opaque"
	if userns.RunningInUserNS() {
		opaqueXattr = "user.overlay.opaque"
	}
	return overlayWhiteoutConverter{
		opaqueXattr: opaqueXattr,
	}
}

func (c overlayWhiteoutConverter) ConvertWrite(hdr *tar.Header, filePath string, fi os.FileInfo) (wo *tar.Header, _ error) {
	// convert whiteouts to AUFS format
	if fi.Mode()&os.ModeCharDevice != 0 && hdr.Devmajor == 0 && hdr.Devminor == 0 {
		// we just rename the file and make it normal
		dir, filename := path.Split(hdr.Name)
		hdr.Name = path.Join(dir, WhiteoutPrefix+filename)
		hdr.Mode = 0o600
		hdr.Typeflag = tar.TypeReg
		hdr.Size = 0
	}

	if !fi.IsDir() {
		// FIXME(thaJeztah): return a sentinel error instead of nil, nil
		return nil, nil
	}

	// convert opaque dirs to AUFS format by writing an empty file with the prefix
	opaque, err := lgetxattr(filePath, c.opaqueXattr)
	if err != nil {
		return nil, err
	}
	if len(opaque) != 1 || opaque[0] != 'y' {
		// FIXME(thaJeztah): return a sentinel error instead of nil, nil
		return nil, nil
	}
	delete(hdr.PAXRecords, paxSchilyXattr+c.opaqueXattr)

	// create a header for the whiteout file
	// it should inherit some properties from the parent, but be a regular file
	return &tar.Header{
		Typeflag:   tar.TypeReg,
		Mode:       hdr.Mode & int64(os.ModePerm),
		Name:       path.Join(hdr.Name, WhiteoutOpaqueDir), // #nosec G305 -- An archive is being created, not extracted.
		Size:       0,
		Uid:        hdr.Uid,
		Uname:      hdr.Uname,
		Gid:        hdr.Gid,
		Gname:      hdr.Gname,
		AccessTime: hdr.AccessTime,
		ChangeTime: hdr.ChangeTime,
	}, nil
}

func (c overlayWhiteoutConverter) ConvertRead(root *os.Root, hdr *tar.Header, filePath string) (bool, error) {
	base := filepath.Base(filePath)
	dir := filepath.Dir(filePath)

	switch base {
	case WhiteoutPrefix, WhiteoutPrefix + ".", WhiteoutPrefix + "..":
		return false, fmt.Errorf("invalid whiteout entry %q", hdr.Name)

	case WhiteoutOpaqueDir:
		parent, err := root.Open(dir)
		if err != nil {
			return false, err
		}
		defer parent.Close()

		// If a directory is marked as opaque by the AUFS special file, we need to translate that to overlay.
		if err := unix.Fsetxattr(int(parent.Fd()), c.opaqueXattr, []byte{'y'}, 0); err != nil {
			return false, fmt.Errorf("fsetxattr('%s', %s=y): %w", dir, c.opaqueXattr, err)
		}
		// Don't write the whiteout file itself.
		return false, nil

	default:
		originalBase, ok := strings.CutPrefix(base, WhiteoutPrefix)
		if !ok {
			// Regular file.
			return true, nil
		}

		parent, err := root.Open(dir)
		if err != nil {
			return false, err
		}
		defer parent.Close()

		// If a file was deleted, and we are using overlay, we need to create a character device.
		originalPath := filepath.Join(dir, originalBase)
		if err := unix.Mknodat(int(parent.Fd()), originalBase, unix.S_IFCHR, 0); err != nil {
			return false, fmt.Errorf("failed to mknod('%s', S_IFCHR, 0): %w", originalPath, err)
		}

		// Header IDs have already been remapped. Optimize the common non-remapped
		// root-owned (0:0) case by assuming the created whiteout has the expected
		// ownership, rather than comparing against the effective UID/GID or stat'ing
		// the created node to verify it.
		if hdr.Uid != 0 || hdr.Gid != 0 {
			// TODO(thaJeztah): Revisit whether whiteout ownership needs to be preserved.
			//
			// This was added in the original overlay whiteout implementation:
			// https://github.com/moby/moby/pull/18560 / https://github.com/moby/moby/pull/22126
			//
			// OverlayFS documents whiteouts in terms of a character device with device
			// number 0:0, not ownership: https://docs.kernel.org/filesystems/overlayfs.html#whiteouts-and-opaque-directories
			//
			// If ownership is not required, this Fchownat can be removed to avoid the remaining TOCTOU window.
			if err := unix.Fchownat(int(parent.Fd()), originalBase, hdr.Uid, hdr.Gid, unix.AT_SYMLINK_NOFOLLOW); err != nil {
				return false, &os.PathError{Op: "lchown", Path: originalPath, Err: err}
			}
		}

		// Don't write the whiteout file itself.
		return false, nil
	}
}
