package main

import (
	"encoding/json"
	"fmt"
	"os"
	"os/exec"
	"path/filepath"
	"reflect"
	"runtime"

	utils "github.com/docker/containerd/testutils"
	ocs "github.com/opencontainers/runtime-spec/specs-go"
)

type OciProcessArgs struct {
	Cmd  string
	Args []string
}

type Bundle struct {
	Source string
	Name   string
	Spec   ocs.Spec
	Path   string
}

var bundleMap map[string]Bundle

// untarRootfs untars the given `source` tarPath into `destination/rootfs`
func untarRootfs(source string, destination string) error {
	var tar *exec.Cmd
	if runtime.GOOS == "solaris" {
		destination = filepath.Join(destination, "/rootfs/root")
		tar = exec.Command("gtar", "-C", destination, "-xf", source)
	} else {
		destination = filepath.Join(destination, "rootfs")
		tar = exec.Command("tar", "-C", destination, "-xf", source)
	}

	if err := os.MkdirAll(destination, 0755); err != nil {
		return nil
	}
	return tar.Run()
}

// CreateBundleWithFilter generate a new oci-bundle named `name` from
// the provide `source` rootfs. It starts from the default spec
// generated by `runc spec`, overrides the `spec.Process.Args` value
// with `args` and set `spec.Process.Terminal` to false. It then apply
// `filter()` to the resulting spec if it is provided.
func CreateBundleWithFilter(source, name string, args []string, filter func(spec *ocs.Spec)) error {
	// Generate the spec
	var spec ocs.Spec
	f, err := os.Open(utils.RefOciSpecsPath)
	if err != nil {
		return fmt.Errorf("Failed to open default spec: %v", err)
	}
	if err := json.NewDecoder(f).Decode(&spec); err != nil {
		return fmt.Errorf("Failed to load default spec: %v", err)
	}
	f.Close()

	spec.Process.Args = args
	spec.Process.Terminal = false
	if filter != nil {
		filter(&spec)
	}

	bundlePath := filepath.Join(utils.BundlesRoot, name)
	nb := Bundle{source, name, spec, bundlePath}

	// Check that we don't already have such a bundle
	if b, ok := bundleMap[name]; ok {
		if reflect.DeepEqual(b, nb) == false {
			return fmt.Errorf("A bundle name named '%s' already exist but with different properties! %#v != %#v",
				name, b, nb)
		}
		return nil
	}

	// Nothing should be there, but just in case
	os.RemoveAll(bundlePath)

	var sourceStr string
	if runtime.GOOS == "solaris" {
		sourceStr = source + ".tar.gz"
	} else {
		sourceStr = source + ".tar"
	}

	if err := untarRootfs(filepath.Join(utils.ArchivesDir, sourceStr), bundlePath); err != nil {
		return fmt.Errorf("Failed to untar %s.tar: %v", source, err)
	}

	// create a place for the io fifo
	if err := os.Mkdir(filepath.Join(bundlePath, "io"), 0755); err != nil {
		return fmt.Errorf("Failed to create bundle io directory: %v", err)
	}

	// Write the updated spec to the right location
	config, e := os.Create(filepath.Join(bundlePath, "config.json"))
	if e != nil {
		return fmt.Errorf("Failed to create oci spec: %v", e)
	}
	defer config.Close()

	if err := json.NewEncoder(config).Encode(&spec); err != nil {
		return fmt.Errorf("Failed to encore oci spec: %v", e)
	}

	bundleMap[name] = nb
	return nil
}

func GetBundle(name string) *Bundle {
	bundle, ok := bundleMap[name]
	if !ok {
		return nil
	}
	return &bundle
}

func CreateBusyboxBundle(name string, args []string) error {
	return CreateBundleWithFilter("busybox", name, args, nil)
}

func CreateSolarisBundle(name string, args []string) error {
	return CreateBundleWithFilter("rootfs", name, args, nil)
}
