package container

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

	"github.com/docker/distribution/reference"
	"github.com/docker/distribution/registry/storage"
	"github.com/docker/distribution/registry/storage/driver/filesystem"
	"github.com/docker/docker/pkg/archive"
	"github.com/genuinetools/reg/repoutils"
	bindata "github.com/jteeuwen/go-bindata"
)

// EmbedImage pulls a docker image locally. Creates a tarball of it's contents
// and then embeds the tarball as binary data into an output bindata.go file.
func EmbedImage(image string) error {
	// Get the current working directory.
	wd, err := os.Getwd()
	if err != nil {
		return err
	}

	// Create our output path
	output := filepath.Join(wd, "bindata.go")

	// Create the temporary directory for the image contents.
	tmpd, err := ioutil.TempDir("", "container-lib")
	if err != nil {
		return err
	}
	defer os.RemoveAll(tmpd) // Cleanup on complete.

	// Create our tarball path.
	tarball := filepath.Join(tmpd, DefaultTarballPath)

	// Create our image root and state.
	root := filepath.Join(tmpd, "root")
	state := filepath.Join(tmpd, "state")

	// Create the rootfs
	if err := createRootFS(image, root, state); err != nil {
		return err
	}

	// Create the tar.
	tar, err := archive.Tar(root, archive.Gzip)
	if err != nil {
		return fmt.Errorf("create tar failed: %v", err)
	}

	// Create the tarball writer.
	writer, err := os.Create(tarball)
	if err != nil {
		return err
	}
	defer writer.Close() // Close the writer.

	if _, err := io.Copy(writer, tar); err != nil {
		return fmt.Errorf("copy tarball failed: %v", err)
	}

	// Create the bindata config.
	bc := bindata.NewConfig()
	bc.Input = []bindata.InputConfig{
		{
			Path:      tarball,
			Recursive: false,
		},
	}
	bc.Output = output
	bc.Package = "main"
	bc.NoMetadata = true
	bc.Prefix = filepath.Dir(tarball)

	if err := bindata.Translate(bc); err != nil {
		return fmt.Errorf("bindata failed: %v", err)
	}

	return nil
}

// createRootFS creates the base filesystem for a docker image.
// It will pull the base image if it does not exist locally.
// This function takes in a image name and the directory where the
// rootfs should be created.
func createRootFS(image, rootfs, state string) error {
	// Create the context.
	ctx := context.Background()

	// Create the new local registry storage.
	local, err := storage.NewRegistry(ctx, filesystem.New(filesystem.DriverParameters{
		RootDirectory: state,
		MaxThreads:    100,
	}))
	if err != nil {
		return fmt.Errorf("creating new registry storage failed: %v", err)
	}

	// Parse the repository name.
	name, err := reference.ParseNormalizedNamed(image)
	if err != nil {
		return fmt.Errorf("not a valid image %q: %v", image, err)
	}
	// Add latest to the image name if it is empty.
	name = reference.TagNameOnly(name)

	// Get the tag for the repo.
	_, tag, err := repoutils.GetRepoAndRef(image)
	if err != nil {
		return err
	}

	// Create the local repository.
	repo, err := local.Repository(ctx, name)
	if err != nil {
		return fmt.Errorf("creating local repository for %q failed: %v", reference.Path(name), err)
	}

	// Create the manifest service.
	ms, err := repo.Manifests(ctx)
	if err != nil {
		return fmt.Errorf("creating manifest service failed: %v", err)
	}

	// Get the specific tag.
	td, err := repo.Tags(ctx).Get(ctx, tag)
	// Check if we got an unknown error, that means the tag does not exist.
	if err != nil && strings.Contains(err.Error(), "unknown") {
		log.Println("image not found locally, pulling the image")

		// Pull the image.
		if err := pull(ctx, local, name, tag); err != nil {
			return fmt.Errorf("pulling failed: %v", err)
		}

		// Try to get the tag again.
		td, err = repo.Tags(ctx).Get(ctx, tag)
	}
	if err != nil {
		return fmt.Errorf("getting local repository tag %q failed: %v", tag, err)
	}

	// Get the specific manifest for the tag.
	manifest, err := ms.Get(ctx, td.Digest)
	if err != nil {
		return fmt.Errorf("getting local manifest for digest %q failed: %v", td.Digest.String(), err)
	}

	blobStore := repo.Blobs(ctx)
	for i, ref := range manifest.References() {
		if i == 0 {
			fmt.Printf("skipping config %v\n", ref.Digest.String())
			continue
		}
		fmt.Printf("unpacking %v\n", ref.Digest.String())
		layer, err := blobStore.Open(ctx, ref.Digest)
		if err != nil {
			return fmt.Errorf("getting blob %q failed: %v", ref.Digest.String(), err)
		}

		// Unpack the tarfile to the mount path.
		// FROM: https://godoc.org/github.com/moby/moby/pkg/archive#TarOptions
		if err := archive.Untar(layer, rootfs, &archive.TarOptions{
			NoLchown: true,
		}); err != nil {
			return fmt.Errorf("error extracting tar for %q: %v", ref.Digest.String(), err)
		}
	}

	return nil
}
