package main

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

	"github.com/containers/buildah/pkg/chrootuser"
	"github.com/containers/buildah/util"
	"github.com/containers/libpod/cmd/podman/cliconfig"
	"github.com/containers/libpod/cmd/podman/libpodruntime"
	"github.com/containers/libpod/libpod"
	"github.com/containers/libpod/libpod/define"
	"github.com/containers/libpod/pkg/cgroups"
	"github.com/containers/libpod/pkg/rootless"
	"github.com/containers/storage"
	"github.com/containers/storage/pkg/archive"
	"github.com/containers/storage/pkg/chrootarchive"
	"github.com/containers/storage/pkg/idtools"
	securejoin "github.com/cyphar/filepath-securejoin"
	"github.com/opencontainers/go-digest"
	"github.com/opencontainers/runtime-spec/specs-go"
	"github.com/pkg/errors"
	"github.com/sirupsen/logrus"
	"github.com/spf13/cobra"
)

var (
	cpCommand cliconfig.CpValues

	cpDescription = `Command copies the contents of SRC_PATH to the DEST_PATH.

  You can copy from the container's file system to the local machine or the reverse, from the local filesystem to the container. If "-" is specified for either the SRC_PATH or DEST_PATH, you can also stream a tar archive from STDIN or to STDOUT. The CONTAINER can be a running or stopped container.  The SRC_PATH or DEST_PATH can be a file or directory.
`
	_cpCommand = &cobra.Command{
		Use:   "cp [flags] SRC_PATH DEST_PATH",
		Short: "Copy files/folders between a container and the local filesystem",
		Long:  cpDescription,
		RunE: func(cmd *cobra.Command, args []string) error {
			cpCommand.InputArgs = args
			cpCommand.GlobalFlags = MainGlobalOpts
			cpCommand.Remote = remoteclient
			return cpCmd(&cpCommand)
		},
		Example: "[CONTAINER:]SRC_PATH [CONTAINER:]DEST_PATH",
	}
)

func init() {
	cpCommand.Command = _cpCommand
	flags := cpCommand.Flags()
	flags.BoolVar(&cpCommand.Extract, "extract", false, "Extract the tar file into the destination directory.")
	flags.BoolVar(&cpCommand.Pause, "pause", copyPause(), "Pause the container while copying")
	cpCommand.SetHelpTemplate(HelpTemplate())
	cpCommand.SetUsageTemplate(UsageTemplate())
}

func cpCmd(c *cliconfig.CpValues) error {
	args := c.InputArgs
	if len(args) != 2 {
		return errors.Errorf("you must provide a source path and a destination path")
	}

	runtime, err := libpodruntime.GetRuntime(getContext(), &c.PodmanCommand)
	if err != nil {
		return errors.Wrapf(err, "could not get runtime")
	}
	defer runtime.DeferredShutdown(false)

	return copyBetweenHostAndContainer(runtime, args[0], args[1], c.Extract, c.Pause)
}

func copyBetweenHostAndContainer(runtime *libpod.Runtime, src string, dest string, extract bool, pause bool) error {

	srcCtr, srcPath := parsePath(runtime, src)
	destCtr, destPath := parsePath(runtime, dest)

	if (srcCtr == nil && destCtr == nil) || (srcCtr != nil && destCtr != nil) {
		return errors.Errorf("invalid arguments %s, %s you must use just one container", src, dest)
	}

	if len(srcPath) == 0 || len(destPath) == 0 {
		return errors.Errorf("invalid arguments %s, %s you must specify paths", src, dest)
	}
	ctr := srcCtr
	isFromHostToCtr := ctr == nil
	if isFromHostToCtr {
		ctr = destCtr
	}

	mountPoint, err := ctr.Mount()
	if err != nil {
		return err
	}
	defer func() {
		if err := ctr.Unmount(false); err != nil {
			logrus.Errorf("unable to umount container '%s': %q", ctr.ID(), err)
		}
	}()

	if pause {
		if err := ctr.Pause(); err != nil {
			// An invalid state error is fine.
			// The container isn't running or is already paused.
			// TODO: We can potentially start the container while
			// the copy is running, which still allows a race where
			// malicious code could mess with the symlink.
			if errors.Cause(err) != define.ErrCtrStateInvalid {
				return err
			}
		} else {
			// Only add the defer if we actually paused
			defer func() {
				if err := ctr.Unpause(); err != nil {
					logrus.Errorf("Error unpausing container after copying: %v", err)
				}
			}()
		}
	}

	user, err := getUser(mountPoint, ctr.User())
	if err != nil {
		return err
	}
	idMappingOpts, err := ctr.IDMappings()
	if err != nil {
		return errors.Wrapf(err, "error getting IDMappingOptions")
	}
	destOwner := idtools.IDPair{UID: int(user.UID), GID: int(user.GID)}
	hostUID, hostGID, err := util.GetHostIDs(convertIDMap(idMappingOpts.UIDMap), convertIDMap(idMappingOpts.GIDMap), user.UID, user.GID)
	if err != nil {
		return err
	}

	hostOwner := idtools.IDPair{UID: int(hostUID), GID: int(hostGID)}

	if isFromHostToCtr {
		if isVol, volDestName, volName := isVolumeDestName(destPath, ctr); isVol { //nolint(gocritic)
			path, err := pathWithVolumeMount(ctr, runtime, volDestName, volName, destPath)
			if err != nil {
				return errors.Wrapf(err, "error getting destination path from volume %s", volDestName)
			}
			destPath = path
		} else if isBindMount, mount := isBindMountDestName(destPath, ctr); isBindMount { //nolint(gocritic)
			path, err := pathWithBindMountSource(mount, destPath)
			if err != nil {
				return errors.Wrapf(err, "error getting destination path from bind mount %s", mount.Destination)
			}
			destPath = path
		} else if filepath.IsAbs(destPath) { //nolint(gocritic)
			cleanedPath, err := securejoin.SecureJoin(mountPoint, destPath)
			if err != nil {
				return err
			}
			destPath = cleanedPath
		} else { //nolint(gocritic)
			ctrWorkDir, err := securejoin.SecureJoin(mountPoint, ctr.WorkingDir())
			if err != nil {
				return err
			}
			if err = idtools.MkdirAllAndChownNew(ctrWorkDir, 0755, hostOwner); err != nil {
				return errors.Wrapf(err, "error creating directory %q", destPath)
			}
			cleanedPath, err := securejoin.SecureJoin(mountPoint, filepath.Join(ctr.WorkingDir(), destPath))
			if err != nil {
				return err
			}
			destPath = cleanedPath
		}
	} else {
		destOwner = idtools.IDPair{UID: os.Getuid(), GID: os.Getgid()}
		if isVol, volDestName, volName := isVolumeDestName(srcPath, ctr); isVol { //nolint(gocritic)
			path, err := pathWithVolumeMount(ctr, runtime, volDestName, volName, srcPath)
			if err != nil {
				return errors.Wrapf(err, "error getting source path from volume %s", volDestName)
			}
			srcPath = path
		} else if isBindMount, mount := isBindMountDestName(srcPath, ctr); isBindMount { //nolint(gocritic)
			path, err := pathWithBindMountSource(mount, srcPath)
			if err != nil {
				return errors.Wrapf(err, "error getting source path from bind mount %s", mount.Destination)
			}
			srcPath = path
		} else if filepath.IsAbs(srcPath) { //nolint(gocritic)
			cleanedPath, err := securejoin.SecureJoin(mountPoint, srcPath)
			if err != nil {
				return err
			}
			srcPath = cleanedPath
		} else { //nolint(gocritic)
			cleanedPath, err := securejoin.SecureJoin(mountPoint, filepath.Join(ctr.WorkingDir(), srcPath))
			if err != nil {
				return err
			}
			srcPath = cleanedPath
		}
	}

	if !filepath.IsAbs(destPath) {
		dir, err := os.Getwd()
		if err != nil {
			return errors.Wrapf(err, "err getting current working directory")
		}
		destPath = filepath.Join(dir, destPath)
	}

	if src == "-" {
		srcPath = os.Stdin.Name()
		extract = true
	}
	return copy(srcPath, destPath, src, dest, idMappingOpts, &destOwner, extract, isFromHostToCtr)
}

func getUser(mountPoint string, userspec string) (specs.User, error) {
	uid, gid, _, err := chrootuser.GetUser(mountPoint, userspec)
	u := specs.User{
		UID:      uid,
		GID:      gid,
		Username: userspec,
	}
	if !strings.Contains(userspec, ":") {
		groups, err2 := chrootuser.GetAdditionalGroupsForUser(mountPoint, uint64(u.UID))
		if err2 != nil {
			if errors.Cause(err2) != chrootuser.ErrNoSuchUser && err == nil {
				err = err2
			}
		} else {
			u.AdditionalGids = groups
		}

	}
	return u, err
}

func parsePath(runtime *libpod.Runtime, path string) (*libpod.Container, string) {
	pathArr := strings.SplitN(path, ":", 2)
	if len(pathArr) == 2 {
		ctr, err := runtime.LookupContainer(pathArr[0])
		if err == nil {
			return ctr, pathArr[1]
		}
	}
	return nil, path
}

func evalSymlinks(path string) (string, error) {
	if path == os.Stdin.Name() {
		return path, nil
	}
	return filepath.EvalSymlinks(path)
}

func getPathInfo(path string) (string, os.FileInfo, error) {
	path, err := evalSymlinks(path)
	if err != nil {
		return "", nil, errors.Wrapf(err, "error evaluating symlinks %q", path)
	}
	srcfi, err := os.Stat(path)
	if err != nil {
		return "", nil, errors.Wrapf(err, "error reading path %q", path)
	}
	return path, srcfi, nil
}

func copy(srcPath, destPath, src, dest string, idMappingOpts storage.IDMappingOptions, chownOpts *idtools.IDPair, extract, isFromHostToCtr bool) error {
	srcPath, err := evalSymlinks(srcPath)
	if err != nil {
		return errors.Wrapf(err, "error evaluating symlinks %q", srcPath)
	}

	srcPath, srcfi, err := getPathInfo(srcPath)
	if err != nil {
		return err
	}

	filename := filepath.Base(destPath)
	if filename == "-" && !isFromHostToCtr {
		err := streamFileToStdout(srcPath, srcfi)
		if err != nil {
			return errors.Wrapf(err, "error streaming source file %s to Stdout", srcPath)
		}
		return nil
	}

	destdir := destPath
	if !srcfi.IsDir() {
		destdir = filepath.Dir(destPath)
	}
	_, err = os.Stat(destdir)
	if err != nil && !os.IsNotExist(err) {
		return errors.Wrapf(err, "error checking directory %q", destdir)
	}
	destDirIsExist := err == nil
	if err = os.MkdirAll(destdir, 0755); err != nil {
		return errors.Wrapf(err, "error creating directory %q", destdir)
	}

	// return functions for copying items
	copyFileWithTar := chrootarchive.CopyFileWithTarAndChown(chownOpts, digest.Canonical.Digester().Hash(), idMappingOpts.UIDMap, idMappingOpts.GIDMap)
	copyWithTar := chrootarchive.CopyWithTarAndChown(chownOpts, digest.Canonical.Digester().Hash(), idMappingOpts.UIDMap, idMappingOpts.GIDMap)
	untarPath := chrootarchive.UntarPathAndChown(chownOpts, digest.Canonical.Digester().Hash(), idMappingOpts.UIDMap, idMappingOpts.GIDMap)

	if srcfi.IsDir() {
		logrus.Debugf("copying %q to %q", srcPath+string(os.PathSeparator)+"*", dest+string(os.PathSeparator)+"*")
		if destDirIsExist && !strings.HasSuffix(src, fmt.Sprintf("%s.", string(os.PathSeparator))) {
			destPath = filepath.Join(destPath, filepath.Base(srcPath))
		}
		if err = copyWithTar(srcPath, destPath); err != nil {
			return errors.Wrapf(err, "error copying %q to %q", srcPath, dest)
		}
		return nil
	}

	if extract {
		// We're extracting an archive into the destination directory.
		logrus.Debugf("extracting contents of %q into %q", srcPath, destPath)
		if err = untarPath(srcPath, destPath); err != nil {
			return errors.Wrapf(err, "error extracting %q into %q", srcPath, destPath)
		}
		return nil
	}

	destfi, err := os.Stat(destPath)
	if err != nil {
		if !os.IsNotExist(err) || strings.HasSuffix(dest, string(os.PathSeparator)) {
			return errors.Wrapf(err, "failed to get stat of dest path %s", destPath)
		}
	}
	if destfi != nil && destfi.IsDir() {
		destPath = filepath.Join(destPath, filepath.Base(srcPath))
	}

	// Copy the file, preserving attributes.
	logrus.Debugf("copying %q to %q", srcPath, destPath)
	if err = copyFileWithTar(srcPath, destPath); err != nil {
		return errors.Wrapf(err, "error copying %q to %q", srcPath, destPath)
	}
	return nil
}

func convertIDMap(idMaps []idtools.IDMap) (convertedIDMap []specs.LinuxIDMapping) {
	for _, idmap := range idMaps {
		tempIDMap := specs.LinuxIDMapping{
			ContainerID: uint32(idmap.ContainerID),
			HostID:      uint32(idmap.HostID),
			Size:        uint32(idmap.Size),
		}
		convertedIDMap = append(convertedIDMap, tempIDMap)
	}
	return convertedIDMap
}

func streamFileToStdout(srcPath string, srcfi os.FileInfo) error {
	if srcfi.IsDir() {
		tw := tar.NewWriter(os.Stdout)
		err := filepath.Walk(srcPath, func(path string, info os.FileInfo, err error) error {
			if err != nil || !info.Mode().IsRegular() || path == srcPath {
				return err
			}
			hdr, err := tar.FileInfoHeader(info, "")
			if err != nil {
				return err
			}

			if err = tw.WriteHeader(hdr); err != nil {
				return err
			}
			fh, err := os.Open(path)
			if err != nil {
				return err
			}
			defer fh.Close()

			_, err = io.Copy(tw, fh)
			return err
		})
		if err != nil {
			return errors.Wrapf(err, "error streaming directory %s to Stdout", srcPath)
		}
		return nil
	}

	file, err := os.Open(srcPath)
	if err != nil {
		return errors.Wrapf(err, "error opening file %s", srcPath)
	}
	defer file.Close()
	if !archive.IsArchivePath(srcPath) {
		tw := tar.NewWriter(os.Stdout)
		hdr, err := tar.FileInfoHeader(srcfi, "")
		if err != nil {
			return err
		}
		err = tw.WriteHeader(hdr)
		if err != nil {
			return err
		}
		_, err = io.Copy(tw, file)
		if err != nil {
			return errors.Wrapf(err, "error streaming archive %s to Stdout", srcPath)
		}
		return nil
	}

	_, err = io.Copy(os.Stdout, file)
	if err != nil {
		return errors.Wrapf(err, "error streaming file to Stdout")
	}
	return nil
}

func isVolumeDestName(path string, ctr *libpod.Container) (bool, string, string) {
	separator := string(os.PathSeparator)
	if filepath.IsAbs(path) {
		path = strings.TrimPrefix(path, separator)
	}
	if path == "" {
		return false, "", ""
	}
	for _, vol := range ctr.Config().NamedVolumes {
		volNamePath := strings.TrimPrefix(vol.Dest, separator)
		if matchVolumePath(path, volNamePath) {
			return true, vol.Dest, vol.Name
		}
	}
	return false, "", ""
}

// if SRCPATH or DESTPATH is from volume mount's destination -v or --mount type=volume, generates the path with volume mount point
func pathWithVolumeMount(ctr *libpod.Container, runtime *libpod.Runtime, volDestName, volName, path string) (string, error) {
	destVolume, err := runtime.GetVolume(volName)
	if err != nil {
		return "", errors.Wrapf(err, "error getting volume destination %s", volName)
	}
	if !filepath.IsAbs(path) {
		path = filepath.Join(string(os.PathSeparator), path)
	}
	path, err = securejoin.SecureJoin(destVolume.MountPoint(), strings.TrimPrefix(path, volDestName))
	return path, err
}

func isBindMountDestName(path string, ctr *libpod.Container) (bool, specs.Mount) {
	separator := string(os.PathSeparator)
	if filepath.IsAbs(path) {
		path = strings.TrimPrefix(path, string(os.PathSeparator))
	}
	if path == "" {
		return false, specs.Mount{}
	}
	for _, m := range ctr.Config().Spec.Mounts {
		if m.Type != "bind" {
			continue
		}
		mDest := strings.TrimPrefix(m.Destination, separator)
		if matchVolumePath(path, mDest) {
			return true, m
		}
	}
	return false, specs.Mount{}
}

func matchVolumePath(path, target string) bool {
	pathStr := filepath.Clean(path)
	target = filepath.Clean(target)
	for len(pathStr) > len(target) && strings.Contains(pathStr, string(os.PathSeparator)) {
		pathStr = pathStr[:strings.LastIndex(pathStr, string(os.PathSeparator))]
	}
	return pathStr == target
}

func pathWithBindMountSource(m specs.Mount, path string) (string, error) {
	if !filepath.IsAbs(path) {
		path = filepath.Join(string(os.PathSeparator), path)
	}
	return securejoin.SecureJoin(m.Source, strings.TrimPrefix(path, m.Destination))
}

func copyPause() bool {
	if !remoteclient && rootless.IsRootless() {
		cgroupv2, _ := cgroups.IsCgroup2UnifiedMode()
		if !cgroupv2 {
			logrus.Debugf("defaulting to pause==false on rootless cp in cgroupv1 systems")
			return false
		}
	}
	return true
}
