//
// Copyright (c) 2017 Intel Corporation
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//

// Package bat contains a number of helper functions that can be used to perform
// various operations on a ciao cluster such as creating an instance or retrieving
// a list of all the defined workloads, etc.  All of these helper functions are
// implemented by calling ciao rather than by using ciao's REST APIs.  This
// package is mainly intended for use by BAT tests.  Manipulating the cluster
// via ciao, rather than through the REST APIs, allows us to test a little
// bit more of ciao.
package bat

import (
	"context"
	"fmt"
	"io/ioutil"
	"os"
	"path"

	"github.com/ciao-project/ciao/ciao-controller/types"
	"gopkg.in/yaml.v2"
)

// Source is provided to the disk structure to indicate whether the
// disk should be cloned from a volume or an image.
type Source struct {
	Type   string `yaml:"type"`
	Source string `yaml:"source"`
}

// Disk describes the storage for the workload definition.
type Disk struct {
	ID        *string `yaml:"volume_id"`
	Size      int     `yaml:"size"`
	Bootable  bool    `yaml:"bootable"`
	Source    *Source `yaml:"source"`
	Ephemeral bool    `yaml:"ephemeral"`
}

// WorkloadRequirements indicate how many cpus and mem to allocate.
type WorkloadRequirements struct {
	VCPUs      int    `yaml:"vcpus"`
	MemMB      int    `yaml:"mem_mb"`
	NodeID     string `yaml:"node_id,omitempty"`
	Hostname   string `yaml:"hostname,omitempty"`
	Privileged bool   `yaml:"privileged,omitempty"`
}

// WorkloadOptions is used to generate a workload definition in yaml.
type WorkloadOptions struct {
	Description     string               `yaml:"description"`
	VMType          string               `yaml:"vm_type"`
	FWType          string               `yaml:"fw_type"`
	ImageName       string               `yaml:"image_name"`
	Requirements    WorkloadRequirements `yaml:"requirements"`
	CloudConfigFile string               `yaml:"cloud_init"`
	Disks           []Disk               `yaml:"disks"`
}

// this function should output the yaml file for the workload definition
func createWorkloadDefinition(filename string, opt WorkloadOptions) error {
	// take the cloud config file and output it to a file.
	y, err := yaml.Marshal(opt)
	if err != nil {
		return err
	}

	return ioutil.WriteFile(filename, y, 0644)
}

// this function should output a cloud init file for the workload definition
func createCloudConfig(filename string, config string) error {
	// take the cloud config file and output it to a file.
	// convert string to bytes.
	return ioutil.WriteFile(filename, []byte(config), 0644)
}

func createWorkload(ctx context.Context, tenant string, opt WorkloadOptions, config string, public bool) (ID string, err error) {
	// create temp dir for the yaml files
	dir, err := ioutil.TempDir("", "ciao_bat")
	if err != nil {
		return "", err
	}
	defer func() {
		os.RemoveAll(dir)
	}()

	opt.CloudConfigFile = path.Join(dir, "config.yaml")

	// write out the cloud config.
	err = createCloudConfig(opt.CloudConfigFile, config)
	if err != nil {
		return "", err
	}

	// write out the workload definition
	fp := path.Join(dir, "workload.yaml")
	err = createWorkloadDefinition(fp, opt)
	if err != nil {
		return "", err
	}

	return CreateWorkloadFromFile(ctx, public, tenant, fp)
}

func deleteWorkload(ctx context.Context, tenant string, workload string, public bool) (err error) {
	args := []string{"delete", "workload", workload}
	if public {
		_, err = RunCIAOCmdAsAdmin(ctx, tenant, args)
	} else {
		_, err = RunCIAOCmd(ctx, tenant, args)
	}

	return err
}

// CreatePublicWorkload will call ciao as admin to create a workload.
// It will first output the cloud init yaml file to the current working
// directory. Then it will output the workload definition to the current
// working directory. Finally it will call ciao create workload
// to upload the workload definition. It will clean up all the files it
// created when it is done.
func CreatePublicWorkload(ctx context.Context, tenant string, opt WorkloadOptions, config string) (string, error) {
	return createWorkload(ctx, "", opt, config, true)
}

// CreateWorkloadFromFile will call ciao create workload
// to create a workload from the specified file.  ciao will be invoked
// using the admin user if the public flag is set to true.  The provided
// workload definition file will remain intact after the call returns.
// The id of the new workload is returned upon success.
func CreateWorkloadFromFile(ctx context.Context, public bool, tenant, wdPath string) (string, error) {
	args := []string{"create", "workload", wdPath, "-f", "{{ tojson .}}"}

	var err error
	var workload types.Workload

	if public {
		err = RunCIAOCmdAsAdminJS(ctx, tenant, args, &workload)
	} else {
		err = RunCIAOCmdJS(ctx, tenant, args, &workload)
	}

	if err != nil {
		return "", err
	}

	return workload.ID, err
}

// DeletePublicWorkload will call ciao as admin to delete a workload.
func DeletePublicWorkload(ctx context.Context, workload string) error {
	return deleteWorkload(ctx, "", workload, true)
}

// DeleteWorkload will call ciao as a tenant to delete a workload.
func DeleteWorkload(ctx context.Context, tenant string, workload string) error {
	return deleteWorkload(ctx, tenant, workload, false)
}

// CreateWorkload will call ciao to create a workload definition.
// It will first output the cloud init yaml file to the current working
// directory. Then it will output the workload definition to the current
// working directory. Finally it will call ciao create workload
// to upload the workload definition. It will clean up all the files it
// created when it is done.
func CreateWorkload(ctx context.Context, tenant string, opt WorkloadOptions, config string) (string, error) {
	return createWorkload(ctx, tenant, opt, config, false)
}

// GetAllWorkloads retrieves a list of all workloads in the cluster by calling
// ciao list workloads . An error will be returned if the following
// environment variables are not set; CIAO_CLIENT_CERT_FILE, CIAO_CONTROLLER.
func GetAllWorkloads(ctx context.Context, tenant string) ([]Workload, error) {
	var workloads []Workload

	args := []string{"list", "workloads", "-f", "{{tojson .}}"}
	err := RunCIAOCmdJS(ctx, tenant, args, &workloads)
	if err != nil {
		return nil, err
	}

	return workloads, nil
}

// GetWorkloadByName will return a specific workload referenced by name.
// An error will be returned if either no workloads exist in the cluster,
// or if the specific workload does not exist. It inherits all error
// conditions of GetAllWorkloads.
func GetWorkloadByName(ctx context.Context, tenant string, name string) (Workload, error) {
	wls, err := GetAllWorkloads(ctx, tenant)
	if err != nil {
		return Workload{}, err
	}

	if len(wls) == 0 {
		return Workload{}, fmt.Errorf("No workloads defined for tenant %s", tenant)
	}

	for _, w := range wls {
		if w.Name == name {
			return w, nil
		}
	}

	return Workload{}, fmt.Errorf("No matching workload for %s", name)
}

// GetWorkloadByID will return a specific workload referenced by name.
// An error will be returned if either no workloads exist in the cluster,
// or if the specific workload does not exist. It inherits all error
// conditions of GetAllWorkloads.
func GetWorkloadByID(ctx context.Context, tenant string, ID string) (Workload, error) {
	wls, err := GetAllWorkloads(ctx, tenant)
	if err != nil {
		return Workload{}, err
	}

	if len(wls) == 0 {
		return Workload{}, fmt.Errorf("No workloads defined for tenant %s", tenant)
	}

	for _, w := range wls {
		if w.ID == ID {
			return w, nil
		}
	}

	return Workload{}, fmt.Errorf("No matching workload for %s", ID)
}
