package docker

import (
	"bytes"
	"context"
	"fmt"
	"io"
	"io/ioutil"
	"log"
	"os"
	"path/filepath"

	packersdk "github.com/hashicorp/packer-plugin-sdk/packer"
)

// Windows containers are a special beast in Docker; you can't use docker cp
// to move files between the container and host.

// This communicator works around that limitation by reusing all possible
// methods and fields of the normal Docker Communicator, but we overwrite the
// Upload, Download, and UploadDir methods to utilize a mounted directory and
// native powershell commands rather than relying on docker cp.

type WindowsContainerCommunicator struct {
	Communicator
}

// Upload uses docker exec to copy the file from the host to the container
func (c *WindowsContainerCommunicator) Upload(dst string, src io.Reader, fi *os.FileInfo) error {
	// Create a temporary file to store the upload
	tempfile, err := ioutil.TempFile(c.HostDir, "upload")
	if err != nil {
		return err
	}
	defer os.Remove(tempfile.Name())

	// Copy the contents to the temporary file
	_, err = io.Copy(tempfile, src)
	if err != nil {
		return err
	}
	if fi != nil {
		tempfile.Chmod((*fi).Mode())
	}
	tempfile.Close()

	// Copy the file into place by copying the temporary file we put
	// into the shared folder into the proper location in the container
	cmd := &packersdk.RemoteCmd{
		Command: fmt.Sprintf("Copy-Item -Path %s/%s -Destination %s", c.ContainerDir,
			filepath.Base(tempfile.Name()), dst),
	}
	ctx := context.TODO()
	if err := c.Start(ctx, cmd); err != nil {
		return err
	}

	// Wait for the copy to complete
	cmd.Wait()
	if cmd.ExitStatus() != 0 {
		return fmt.Errorf("Upload failed with non-zero exit status: %d", cmd.ExitStatus())
	}

	return nil
}

func (c *WindowsContainerCommunicator) UploadDir(dst string, src string, exclude []string) error {
	// Create the temporary directory that will store the contents of "src"
	// for copying into the container.
	td, err := ioutil.TempDir(c.HostDir, "dirupload")
	if err != nil {
		return err
	}
	defer os.RemoveAll(td)

	walkFn := func(path string, info os.FileInfo, err error) error {
		if err != nil {
			return err
		}

		relpath, err := filepath.Rel(src, path)
		if err != nil {
			return err
		}
		hostpath := filepath.Join(td, relpath)

		// If it is a directory, just create it
		if info.IsDir() {
			return os.MkdirAll(hostpath, info.Mode())
		}

		if info.Mode()&os.ModeSymlink == os.ModeSymlink {
			dest, err := os.Readlink(path)

			if err != nil {
				return err
			}

			return os.Symlink(dest, hostpath)
		}

		// It is a file, copy it over, including mode.
		src, err := os.Open(path)
		if err != nil {
			return err
		}
		defer src.Close()

		dst, err := os.Create(hostpath)
		if err != nil {
			return err
		}
		defer dst.Close()

		if _, err := io.Copy(dst, src); err != nil {
			return err
		}

		return nil
	}

	// Copy the entire directory tree to the temporary directory
	if err := filepath.Walk(src, walkFn); err != nil {
		return err
	}

	// Determine the destination directory
	containerSrc := filepath.Join(c.ContainerDir, filepath.Base(td))
	containerDst := dst
	if src[len(src)-1] != '/' {
		containerDst = filepath.Join(dst, filepath.Base(src))
	}

	// Make the directory, then copy into it
	cmd := &packersdk.RemoteCmd{
		Command: fmt.Sprintf("Copy-Item %s -Destination %s -Recurse",
			containerSrc, containerDst),
	}
	ctx := context.TODO()
	if err := c.Start(ctx, cmd); err != nil {
		return err
	}

	// Wait for the copy to complete
	cmd.Wait()
	if cmd.ExitStatus() != 0 {
		return fmt.Errorf("Upload failed with non-zero exit status: %d", cmd.ExitStatus())
	}

	return nil
}

// Download pulls a file out of a container using `docker cp`. We have a source
// path and want to write to an io.Writer
func (c *WindowsContainerCommunicator) Download(src string, dst io.Writer) error {
	log.Printf("Downloading file from container: %s:%s", c.ContainerID, src)
	// Copy file onto temp file on mounted volume inside container
	var stdout, stderr bytes.Buffer
	cmd := &packersdk.RemoteCmd{
		Command: fmt.Sprintf("Copy-Item -Path %s -Destination %s/%s", src, c.ContainerDir,
			filepath.Base(src)),
		Stdout: &stdout,
		Stderr: &stderr,
	}
	ctx := context.TODO()
	if err := c.Start(ctx, cmd); err != nil {
		return err
	}

	// Wait for the copy to complete
	cmd.Wait()

	if cmd.ExitStatus() != 0 {
		return fmt.Errorf("Failed to copy file to shared drive: %s, %s, %d", stderr.String(), stdout.String(), cmd.ExitStatus())
	}

	// Read that copied file into a new file opened on host machine
	fsrc, err := os.Open(filepath.Join(c.HostDir, filepath.Base(src)))
	if err != nil {
		return err
	}
	defer fsrc.Close()
	defer os.Remove(fsrc.Name())

	_, err = io.Copy(dst, fsrc)
	if err != nil {
		return err
	}

	return nil
}
