/*
Copyright 2018 The Kubernetes Authors.

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 gce

import (
	"fmt"
	"os/exec"
	"time"

	"k8s.io/node-problem-detector/test/e2e/lib/ssh"

	. "github.com/onsi/gomega"
	compute "google.golang.org/api/compute/v1"
)

// Instance represents a GCE instance.
type Instance struct {
	Name           string
	Zone           string
	Project        string
	MachineType    string
	ExternalIP     string
	SshKey         string
	SshUser        string
	ComputeService *compute.Service
}

// CreateInstance creates a GCE instance with provided spec.
func CreateInstance(instance Instance, imageName string, imageProject string) (Instance, error) {
	if instance.MachineType == "" {
		instance.MachineType = "n1-standard-1"
	}

	p, err := instance.ComputeService.Projects.Get(instance.Project).Do()
	if err != nil {
		return instance, fmt.Errorf("failed to get project info %q: %v", instance.Project, err)
	}

	i := &compute.Instance{
		Name:        instance.Name,
		MachineType: fmt.Sprintf("zones/%s/machineTypes/%s", instance.Zone, instance.MachineType),
		NetworkInterfaces: []*compute.NetworkInterface{
			{
				AccessConfigs: []*compute.AccessConfig{
					{
						Type: "ONE_TO_ONE_NAT",
						Name: "External NAT",
					},
				}},
		},
		Disks: []*compute.AttachedDisk{
			{
				AutoDelete: true,
				Boot:       true,
				Type:       "PERSISTENT",
				InitializeParams: &compute.AttachedDiskInitializeParams{
					SourceImage: fmt.Sprintf("projects/%s/global/images/%s", imageProject, imageName),
					DiskSizeGb:  20,
				},
			},
		},
		ServiceAccounts: []*compute.ServiceAccount{
			{
				Email: p.DefaultServiceAccount,
				Scopes: []string{
					"https://www.googleapis.com/auth/cloud-platform",
				},
			},
		},
	}

	if _, err := instance.ComputeService.Instances.Get(instance.Project, instance.Zone, instance.Name).Do(); err != nil {
		op, err := instance.ComputeService.Instances.Insert(instance.Project, instance.Zone, i).Do()
		if err != nil {
			ret := fmt.Sprintf("could not create instance %s: API error: %v", instance.Name, err)
			if op != nil {
				ret = fmt.Sprintf("%s: %v", ret, op.Error)
			}
			return instance, fmt.Errorf(ret)
		} else if op.Error != nil {
			return instance, fmt.Errorf("could not create instance %s: %+v", instance.Name, op.Error)
		}
	}

	instanceRunning := false
	// Waiting for the instance to be SSH-able by retrying SSH with timeout of 5 min (15*20sec)
	for i := 0; i < 15 && !instanceRunning; i++ {
		if i > 0 {
			time.Sleep(time.Second * 20)
		}
		if instance.ExternalIP == "" {
			instance.populateExternalIP()
		}

		result := ssh.Run("pwd", instance.ExternalIP, instance.SshUser, instance.SshKey)
		if result.SSHError != nil {
			err = fmt.Errorf("SSH to instance %s failed: %s", instance.Name, result.SSHError)
			continue
		}
		instanceRunning = true
	}

	if !instanceRunning {
		return instance, err
	}
	return instance, nil
}

func (ins *Instance) populateExternalIP() {
	gceInstance, err := ins.ComputeService.Instances.Get(ins.Project, ins.Zone, ins.Name).Do()
	if err != nil {
		return
	}

	for i := range gceInstance.NetworkInterfaces {
		ni := gceInstance.NetworkInterfaces[i]
		for j := range ni.AccessConfigs {
			ac := ni.AccessConfigs[j]
			if len(ac.NatIP) > 0 {
				ins.ExternalIP = ac.NatIP
				return
			}
		}
	}
}

// RunCommand runs a command on the GCE instance and returns the command result.
func (ins *Instance) RunCommand(cmd string) ssh.Result {
	if ins.ExternalIP == "" {
		ins.populateExternalIP()
	}
	return ssh.Run(cmd, ins.ExternalIP, ins.SshUser, ins.SshKey)
}

// RunCommand runs a command on the GCE instance and returns the command result, and fails the test when the command failed.
func (ins *Instance) RunCommandOrFail(cmd string) ssh.Result {
	result := ins.RunCommand(cmd)
	Expect(result.SSHError).ToNot(HaveOccurred(), "SSH-ing to the instance failed: %v\n", result)
	Expect(result.Code).To(Equal(0), "Running command failed: %v\n", result)
	return result
}

// PushFile pushes a local file to a GCE instance.
func (ins *Instance) PushFile(srcPath, destPath string) error {
	if ins.ExternalIP == "" {
		ins.populateExternalIP()
	}
	output, err := exec.Command("scp", "-o", "StrictHostKeyChecking no",
		"-i", ins.SshKey,
		srcPath, fmt.Sprintf("%s@%s:%s", ins.SshUser, ins.ExternalIP, destPath)).CombinedOutput()
	if err != nil {
		return fmt.Errorf("Error running scp: %v.\nHere is the output for the command: %v", err, string(output))
	}
	return nil
}

// DeleteInstance deletes a GCE instance.
func (ins *Instance) DeleteInstance() error {
	if _, err := ins.ComputeService.Instances.Get(ins.Project, ins.Zone, ins.Name).Do(); err != nil {
		return err
	}
	if _, err := ins.ComputeService.Instances.Delete(ins.Project, ins.Zone, ins.Name).Do(); err != nil {
		return err
	}
	return nil
}
