//+build windows

package windows

import (
	"context"
	"fmt"
	"os"
	"path/filepath"
	"strings"

	"github.com/Microsoft/hcsshim"
	"github.com/containerd/containerd/errdefs"
	"github.com/containerd/containerd/log"
	specs "github.com/opencontainers/runtime-spec/specs-go"
	"github.com/pkg/errors"
)

// newContainerConfig generates a hcsshim container configuration from the
// provided OCI Spec
func newContainerConfig(ctx context.Context, owner, id string, spec *specs.Spec) (*hcsshim.ContainerConfig, error) {
	var (
		conf = &hcsshim.ContainerConfig{
			SystemType: "Container",
			Name:       id,
			Owner:      owner,
			HostName:   spec.Hostname,
		}
	)

	if spec.Windows.Network != nil {
		conf.AllowUnqualifiedDNSQuery = spec.Windows.Network.AllowUnqualifiedDNSQuery
		conf.EndpointList = spec.Windows.Network.EndpointList
		conf.NetworkSharedContainerName = spec.Windows.Network.NetworkSharedContainerName
		if spec.Windows.Network.DNSSearchList != nil {
			conf.DNSSearchList = strings.Join(spec.Windows.Network.DNSSearchList, ",")
		}
	}

	return conf, nil
}

// newWindowsContainerConfig generates a hcsshim Windows container
// configuration from the provided OCI Spec
func newWindowsContainerConfig(ctx context.Context, owner, id string, spec *specs.Spec) (*hcsshim.ContainerConfig, error) {
	conf, err := newContainerConfig(ctx, owner, id, spec)
	if err != nil {
		return nil, err
	}
	conf.IgnoreFlushesDuringBoot = spec.Windows.IgnoreFlushesDuringBoot

	if len(spec.Windows.LayerFolders) < 1 {
		return nil, errors.Wrap(errdefs.ErrInvalidArgument,
			"spec.Windows.LayerFolders must have at least 1 layers")
	}
	var (
		layerFolders    = spec.Windows.LayerFolders
		homeDir         = filepath.Dir(layerFolders[0])
		layerFolderPath = filepath.Join(homeDir, id)
	)

	// TODO: use the create request Mount for those
	for _, layerPath := range layerFolders {
		_, filename := filepath.Split(layerPath)
		guid, err := hcsshim.NameToGuid(filename)
		if err != nil {
			return nil, errors.Wrapf(err, "unable to get GUID for %s", filename)
		}
		conf.Layers = append(conf.Layers, hcsshim.Layer{
			ID:   guid.ToString(),
			Path: layerPath,
		})
	}

	var (
		di = hcsshim.DriverInfo{
			Flavour: 1, // filter driver
			HomeDir: homeDir,
		}
	)
	conf.LayerFolderPath = layerFolderPath

	// TODO: Once there is a snapshotter for windows, this can be deleted.
	// The R/W Layer should come from the Rootfs Mounts provided
	//
	// Windows doesn't support creating a container with a readonly
	// filesystem, so always create a RW one
	if err = hcsshim.CreateSandboxLayer(di, id, layerFolders[0], layerFolders); err != nil {
		return nil, errors.Wrapf(err, "failed to create sandbox layer for %s: layers: %#v, driverInfo: %#v",
			id, layerFolders, di)
	}
	defer func() {
		if err != nil {
			removeLayer(ctx, conf.LayerFolderPath)
		}
	}()

	if err = hcsshim.ActivateLayer(di, id); err != nil {
		return nil, errors.Wrapf(err, "failed to activate layer %s", conf.LayerFolderPath)
	}

	if err = hcsshim.PrepareLayer(di, id, layerFolders); err != nil {
		return nil, errors.Wrapf(err, "failed to prepare layer %s", conf.LayerFolderPath)
	}

	conf.VolumePath, err = hcsshim.GetLayerMountPath(di, id)
	if err != nil {
		return nil, errors.Wrapf(err, "failed to getmount path for layer %s: driverInfo: %#v", id, di)
	}

	if spec.Windows.HyperV != nil {
		conf.HvPartition = true
		for _, layerPath := range layerFolders {
			utilityVMPath := spec.Windows.HyperV.UtilityVMPath
			_, err := os.Stat(utilityVMPath)
			if err == nil {
				conf.HvRuntime = &hcsshim.HvRuntime{ImagePath: utilityVMPath}
				break
			} else if !os.IsNotExist(err) {
				return nil, errors.Wrapf(err, "failed to access layer %s", layerPath)
			}
		}
	}

	if spec.Windows.CredentialSpec != nil {
		conf.Credentials = spec.Windows.CredentialSpec.(string)
	}

	if len(spec.Mounts) > 0 {
		mds := make([]hcsshim.MappedDir, len(spec.Mounts))
		for i, mount := range spec.Mounts {
			mds[i] = hcsshim.MappedDir{
				HostPath:      mount.Source,
				ContainerPath: mount.Destination,
				ReadOnly:      false,
			}
			for _, o := range mount.Options {
				if strings.ToLower(o) == "ro" {
					mds[i].ReadOnly = true
				}
			}
		}
		conf.MappedDirectories = mds
	}

	return conf, nil
}

// removeLayer deletes the given layer, all associated containers must have
// been shutdown for this to succeed.
func removeLayer(ctx context.Context, path string) error {
	var (
		err        error
		layerID    = filepath.Base(path)
		parentPath = filepath.Dir(path)
		di         = hcsshim.DriverInfo{
			Flavour: 1, // filter driver
			HomeDir: parentPath,
		}
	)

	if err = hcsshim.UnprepareLayer(di, layerID); err != nil {
		log.G(ctx).WithError(err).Warnf("failed to unprepare layer %s for removal", path)
	}

	if err = hcsshim.DeactivateLayer(di, layerID); err != nil {
		log.G(ctx).WithError(err).Warnf("failed to deactivate layer %s for removal", path)
	}

	removePath := filepath.Join(parentPath, fmt.Sprintf("%s-removing", layerID))
	if err = os.Rename(path, removePath); err != nil {
		log.G(ctx).WithError(err).Warnf("failed to rename container layer %s for removal", path)
		removePath = path
	}

	if err = hcsshim.DestroyLayer(di, removePath); err != nil {
		log.G(ctx).WithError(err).Errorf("failed to remove container layer %s", removePath)
		return err
	}

	return nil
}

func newProcessConfig(processSpec *specs.Process, pset *pipeSet) *hcsshim.ProcessConfig {
	conf := &hcsshim.ProcessConfig{
		EmulateConsole:   pset.src.Terminal,
		CreateStdInPipe:  pset.stdin != nil,
		CreateStdOutPipe: pset.stdout != nil,
		CreateStdErrPipe: pset.stderr != nil,
		User:             processSpec.User.Username,
		Environment:      make(map[string]string),
		WorkingDirectory: processSpec.Cwd,
	}

	if processSpec.ConsoleSize != nil {
		conf.ConsoleSize = [2]uint{processSpec.ConsoleSize.Height, processSpec.ConsoleSize.Width}
	}

	// Convert OCI Env format to HCS's
	for _, s := range processSpec.Env {
		arr := strings.SplitN(s, "=", 2)
		if len(arr) == 2 {
			conf.Environment[arr[0]] = arr[1]
		}
	}

	return conf
}

func newWindowsProcessConfig(processSpec *specs.Process, pset *pipeSet) *hcsshim.ProcessConfig {
	conf := newProcessConfig(processSpec, pset)
	conf.CommandLine = strings.Join(processSpec.Args, " ")
	return conf
}
