package mgr

import (
	"context"
	"errors"
	"io"
	"os"
	"path/filepath"
	"strconv"
	"strings"

	"github.com/alibaba/pouch/apis/types"
	"github.com/alibaba/pouch/pkg/ioutils"
	"github.com/alibaba/pouch/pkg/log"

	"github.com/docker/docker/pkg/archive"
	"github.com/docker/docker/pkg/chrootarchive"
	"github.com/docker/docker/pkg/fileutils"
	"github.com/docker/docker/pkg/mount"
	"github.com/go-openapi/strfmt"
	pkgerrors "github.com/pkg/errors"
)

// StatPath stats the dir info at the specified path in the container.
func (mgr *ContainerManager) StatPath(ctx context.Context, name, path string) (stat *types.ContainerPathStat, err error) {
	c, err := mgr.container(name)
	if err != nil {
		return nil, err
	}

	ctx = log.AddFields(ctx, map[string]interface{}{"ContainerID": c.ID})

	c.Lock()
	defer c.Unlock()

	if c.State.Dead {
		return nil, pkgerrors.Errorf("container(%s) has been deleted", c.ID)
	}

	running := c.IsRunningOrPaused()
	if !running {
		if err := mgr.Mount(ctx, c); err != nil {
			return nil, pkgerrors.Wrapf(err, "failed to mount cid(%s)", c.ID)
		}

		defer mgr.Unmount(ctx, c)

		err = mgr.attachVolumes(ctx, c)
		if err != nil {
			return nil, pkgerrors.Wrapf(err, "failed to attachVolumes cid(%s)", c.ID)
		}
		defer mgr.detachVolumes(ctx, c, false)
	}

	err = c.mountVolumes(ctx, running)
	if err != nil {
		return nil, pkgerrors.Wrapf(err, "failed to mountVolumes cid(%s)", c.ID)
	}
	defer c.unmountVolumes(ctx, running)

	resolvedPath, absPath := c.getResolvedPath(path, running)
	lstat, err := os.Lstat(resolvedPath)

	if err != nil {
		return nil, err
	}

	return &types.ContainerPathStat{
		Name:  lstat.Name(),
		Path:  absPath,
		Size:  strconv.FormatInt(lstat.Size(), 10),
		Mode:  uint32(lstat.Mode()),
		Mtime: strfmt.DateTime(lstat.ModTime()),
	}, nil
}

// ArchivePath return an archive and dir info at the specified path in the container.
func (mgr *ContainerManager) ArchivePath(ctx context.Context, name, path string) (content io.ReadCloser, stat *types.ContainerPathStat, err0 error) {
	c, err := mgr.container(name)
	if err != nil {
		return nil, nil, err
	}

	ctx = log.AddFields(ctx, map[string]interface{}{"ContainerID": c.ID})

	c.Lock()
	defer func() {
		if err0 != nil {
			c.Unlock()
		}
	}()

	if c.State.Dead {
		return nil, nil, pkgerrors.Errorf("container(%s) has been deleted", c.ID)
	}

	running := c.IsRunningOrPaused()
	if !running {
		if err := mgr.Mount(ctx, c); err != nil {
			return nil, nil, pkgerrors.Wrapf(err, "failed to mount cid(%s)", c.ID)
		}
		defer func() {
			if err0 != nil {
				mgr.Unmount(ctx, c)
			}
		}()

		err = mgr.attachVolumes(ctx, c)
		if err != nil {
			return nil, nil, pkgerrors.Wrapf(err, "failed to attachVolumes cid(%s)", c.ID)
		}
		defer func() {
			if err0 != nil {
				mgr.detachVolumes(ctx, c, false)
			}
		}()
	}

	err = c.mountVolumes(ctx, running)
	if err != nil {
		return nil, nil, pkgerrors.Wrapf(err, "failed to mountVolumes cid(%s)", c.ID)
	}
	defer func() {
		if err0 != nil {
			c.unmountVolumes(ctx, running)
		}
	}()

	resolvedPath, absPath := c.getResolvedPath(path, running)
	lstat, err := os.Lstat(resolvedPath)
	if err != nil {
		return nil, nil, err
	}

	stat = &types.ContainerPathStat{
		Name:  lstat.Name(),
		Path:  absPath,
		Size:  strconv.FormatInt(lstat.Size(), 10),
		Mode:  uint32(lstat.Mode()),
		Mtime: strfmt.DateTime(lstat.ModTime()),
	}
	// TODO: support follow link in container rootfs
	copyInfo, err := archive.CopyInfoSourcePath(resolvedPath, false)
	if err != nil {
		return nil, nil, err
	}
	data, err := archive.TarResource(copyInfo)
	if err != nil {
		return nil, nil, err
	}

	// wait for io finish, then unmount the rootfs
	content = ioutils.NewReadCloserWrapper(data, func() error {
		err := data.Close()
		if !running {
			mgr.detachVolumes(ctx, c, false)
		}
		c.unmountVolumes(ctx, running)
		mgr.Unmount(ctx, c)
		c.Unlock()
		return err
	})
	mgr.LogContainerEvent(ctx, c, "archive-path")

	return content, stat, nil
}

// ExtractToDir extracts the given archive at the specified path in the container.
func (mgr *ContainerManager) ExtractToDir(ctx context.Context, name, path string, copyUIDGID, noOverwriteDirNonDir bool, content io.Reader) error {
	c, err := mgr.container(name)
	if err != nil {
		return err
	}

	ctx = log.AddFields(ctx, map[string]interface{}{"ContainerID": c.ID})

	c.Lock()
	defer c.Unlock()

	if c.State.Dead {
		return pkgerrors.Errorf("container(%s) has been deleted", c.ID)
	}

	running := c.IsRunningOrPaused()
	if !running {
		if err := mgr.Mount(ctx, c); err != nil {
			return pkgerrors.Wrapf(err, "failed to mount cid(%s)", c.ID)
		}
		defer mgr.Unmount(ctx, c)

		err = mgr.attachVolumes(ctx, c)
		if err != nil {
			return pkgerrors.Wrapf(err, "failed to attachVolumes cid(%s)", c.ID)
		}
		defer mgr.detachVolumes(ctx, c, false)
	}

	err = c.mountVolumes(ctx, running)
	if err != nil {
		return pkgerrors.Wrapf(err, "failed to mountVolumes cid(%s)", c.ID)
	}
	defer c.unmountVolumes(ctx, running)

	resolvedPath, _ := c.getResolvedPath(path, running)

	lstat, err := os.Lstat(resolvedPath)
	if err != nil {
		return err
	}

	if !lstat.IsDir() {
		return errors.New("can't extract to not dir position")
	}

	// first check if the dir in volume
	inVolume := false
	for _, mp := range c.Mounts {
		if !strings.HasPrefix(path, mp.Destination) {
			continue
		}
		inVolume = true
		if mp.RW {
			break
		}
		return errors.New("can't extract to dir because volume read only")
	}

	if !inVolume && c.HostConfig.ReadonlyRootfs {
		return errors.New("can't extract to dir because rootfs read only")
	}

	// TODO: support copy uid/gid maps
	opts := &archive.TarOptions{
		NoOverwriteDirNonDir: noOverwriteDirNonDir,
	}

	mgr.LogContainerEvent(ctx, c, "extract-to-dir")

	return chrootarchive.Untar(content, resolvedPath, opts)
}

func (c *Container) getResolvedPath(path string, running bool) (resolvedPath, absPath string) {
	// consider the given path as an absolute path in the container.
	absPath = path
	if !filepath.IsAbs(absPath) {
		absPath = archive.PreserveTrailingDotOrSeparator(filepath.Join(string(os.PathSeparator), path), path, os.PathSeparator)
	}

	rootfs := c.MountFS
	if running {
		rootfs = c.BaseFS
	}

	// get the real path on the host
	resolvedPath = filepath.Join(rootfs, absPath)
	resolvedPath = filepath.Clean(resolvedPath)

	return resolvedPath, absPath
}

func (c *Container) mountVolumes(ctx context.Context, running bool) (err0 error) {
	rollbackMounts := make([]string, 0, len(c.Mounts))

	defer func() {
		if err0 != nil {
			for _, dest := range rollbackMounts {
				if err := mount.Unmount(dest); err != nil {
					log.With(ctx).Warnf("[mountVolumes:rollback] failed to unmount(%s), err(%v)", dest, err)
				} else {
					log.With(ctx).Debugf("[mountVolumes:rollback] unmount(%s)", dest)
				}
			}
		}
	}()

	for _, m := range c.Mounts {
		dest, _ := c.getResolvedPath(m.Destination, running)

		log.With(ctx).Debugf("try to mount volume(source %s -> dest %s", m.Source, dest)

		stat, err := os.Stat(m.Source)
		if err != nil {
			return err
		}

		// /dev /proc /tmp is mounted by default oci spec. Those mount
		// points are mounted by runC. In k8s, the daemonset will mount
		// log into /dev/termination-log. Before mount it, we need to
		// create folder or empty file for the target. It will not
		// impact the running container.
		if err = fileutils.CreateIfNotExists(dest, stat.IsDir()); err != nil {
			return err
		}

		writeMode := "ro"
		if m.RW {
			writeMode = "rw"
		}

		// mountVolumes() seems to be called for temporary mounts
		// outside the container. Soon these will be unmounted with
		// lazy unmount option and given we have mounted the rbind,
		// all the submounts will propagate if these are shared. If
		// daemon is running in host namespace and has / as shared
		// then these unmounts will propagate and unmount original
		// mount as well. So make all these mounts rprivate.
		// Do not use propagation property of volume as that should
		// apply only when mounting happens inside the container.
		opts := strings.Join([]string{"bind", writeMode, "rprivate"}, ",")
		if err := mount.Mount(m.Source, dest, "", opts); err != nil {
			return err
		}

		rollbackMounts = append(rollbackMounts, dest)
	}

	return nil
}

func (c *Container) unmountVolumes(ctx context.Context, running bool) error {
	for _, m := range c.Mounts {
		dest, _ := c.getResolvedPath(m.Destination, running)

		if err := mount.Unmount(dest); err != nil {
			return err
		}
	}
	return nil
}
