package vagrant

import (
	"archive/tar"
	"compress/flate"
	"encoding/json"
	"fmt"
	"io"
	"log"
	"os"
	"path/filepath"
	"runtime"

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

var (
	// ErrInvalidCompressionLevel is returned when the compression level passed
	// to gzip is not in the expected range. See compress/flate for details.
	ErrInvalidCompressionLevel = fmt.Errorf(
		"Invalid compression level. Expected an integer from -1 to 9.")
)

// Copies a file by copying the contents of the file to another place.
func CopyContents(dst, src string) error {
	srcF, err := os.Open(src)
	if err != nil {
		return err
	}
	defer srcF.Close()

	dstDir, _ := filepath.Split(dst)
	if dstDir != "" {
		err := os.MkdirAll(dstDir, 0755)
		if err != nil {
			return err
		}
	}

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

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

	return nil
}

// Creates a (hard) link to a file, ensuring that all parent directories also exist.
func LinkFile(dst, src string) error {
	dstDir, _ := filepath.Split(dst)
	if dstDir != "" {
		err := os.MkdirAll(dstDir, 0755)
		if err != nil {
			return err
		}
	}

	if err := os.Link(src, dst); err != nil {
		return err
	}

	return nil
}

// DirToBox takes the directory and compresses it into a Vagrant-compatible
// box. This function does not perform checks to verify that dir is
// actually a proper box. This is an expected precondition.
func DirToBox(dst, dir string, ui packersdk.Ui, level int) error {
	log.Printf("Turning dir into box: %s => %s", dir, dst)

	// Make the containing directory, if it does not already exist
	err := os.MkdirAll(filepath.Dir(dst), 0755)
	if err != nil {
		return err
	}

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

	var dstWriter io.WriteCloser = dstF
	if level != flate.NoCompression {
		log.Printf("Compressing with gzip compression level: %d", level)
		gzipWriter, err := makePgzipWriter(dstWriter, level)
		if err != nil {
			return err
		}
		defer gzipWriter.Close()

		dstWriter = gzipWriter
	}

	tarWriter := tar.NewWriter(dstWriter)
	defer tarWriter.Close()

	// This is the walk func that tars each of the files in the dir
	tarWalk := func(path string, info os.FileInfo, prevErr error) error {
		// If there was a prior error, return it
		if prevErr != nil {
			return prevErr
		}

		// Skip directories
		if info.IsDir() {
			log.Printf("Skipping directory '%s' for box '%s'", path, dst)
			return nil
		}

		log.Printf("Box add: '%s' to '%s'", path, dst)
		f, err := os.Open(path)
		if err != nil {
			return err
		}
		defer f.Close()

		header, err := tar.FileInfoHeader(info, "")
		if err != nil {
			return err
		}

		// go >=1.10 wants to use GNU tar format to workaround issues in
		// libarchive < 3.3.2
		setHeaderFormat(header)

		// We have to set the Name explicitly because it is supposed to
		// be a relative path to the root. Otherwise, the tar ends up
		// being a bunch of files in the root, even if they're actually
		// nested in a dir in the original "dir" param.
		header.Name, err = filepath.Rel(dir, path)
		if err != nil {
			return err
		}

		if ui != nil {
			ui.Message(fmt.Sprintf("Compressing: %s", header.Name))
		}

		if err := tarWriter.WriteHeader(header); err != nil {
			return err
		}

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

		return nil
	}

	// Tar.gz everything up
	return filepath.Walk(dir, tarWalk)
}

// CreateDummyBox create a dummy Vagrant-compatible box under temporary dir
// This function is mainly used to check cases such as the host system having
// a GNU tar incompatible uname that will cause the actual Vagrant box creation
// to fail later
func CreateDummyBox(ui packersdk.Ui, level int) error {
	ui.Say("Creating a dummy Vagrant box to ensure the host system can create one correctly")

	// Create a temporary dir to create dummy Vagrant box from
	tempDir, err := tmp.Dir("packer")
	if err != nil {
		return err
	}
	defer os.RemoveAll(tempDir)

	// Write some dummy metadata for the box
	if err := WriteMetadata(tempDir, make(map[string]string)); err != nil {
		return err
	}

	// Create the dummy Vagrant box
	tempBox, err := tmp.File("box-*.box")
	if err != nil {
		return err
	}
	defer tempBox.Close()
	defer os.Remove(tempBox.Name())
	if err := DirToBox(tempBox.Name(), tempDir, nil, level); err != nil {
		return err
	}

	return nil
}

// WriteMetadata writes the "metadata.json" file for a Vagrant box.
func WriteMetadata(dir string, contents interface{}) error {
	if _, err := os.Stat(filepath.Join(dir, "metadata.json")); os.IsNotExist(err) {
		f, err := os.Create(filepath.Join(dir, "metadata.json"))
		if err != nil {
			return err
		}
		defer f.Close()

		enc := json.NewEncoder(f)
		return enc.Encode(contents)
	}

	return nil
}

func makePgzipWriter(output io.WriteCloser, compressionLevel int) (io.WriteCloser, error) {
	gzipWriter, err := pgzip.NewWriterLevel(output, compressionLevel)
	if err != nil {
		return nil, ErrInvalidCompressionLevel
	}
	gzipWriter.SetConcurrency(500000, runtime.GOMAXPROCS(-1))
	return gzipWriter, nil
}
