// Copyright (C) The Arvados Authors. All rights reserved.
//
// SPDX-License-Identifier: AGPL-3.0

package cloudtest

import (
	"bufio"
	"errors"
	"flag"
	"fmt"
	"io"
	"os"

	"git.arvados.org/arvados.git/lib/cloud"
	"git.arvados.org/arvados.git/lib/cmd"
	"git.arvados.org/arvados.git/lib/config"
	"git.arvados.org/arvados.git/lib/dispatchcloud"
	"git.arvados.org/arvados.git/sdk/go/arvados"
	"git.arvados.org/arvados.git/sdk/go/ctxlog"
)

var Command command

type command struct{}

func (command) RunCommand(prog string, args []string, stdin io.Reader, stdout, stderr io.Writer) int {
	var err error
	defer func() {
		if err != nil {
			fmt.Fprintf(stderr, "%s\n", err)
		}
	}()

	flags := flag.NewFlagSet("", flag.ContinueOnError)
	flags.SetOutput(stderr)
	configFile := flags.String("config", arvados.DefaultConfigFile, "Site configuration `file`")
	instanceSetID := flags.String("instance-set-id", "zzzzz-zzzzz-zzzzzzcloudtest", "InstanceSetID tag `value` to use on the test instance")
	imageID := flags.String("image-id", "", "Image ID to use when creating the test instance (if empty, use cluster config)")
	instanceType := flags.String("instance-type", "", "Instance type to create (if empty, use cheapest type in config)")
	destroyExisting := flags.Bool("destroy-existing", false, "Destroy any existing instances tagged with our InstanceSetID, instead of erroring out")
	shellCommand := flags.String("command", "", "Run an interactive shell command on the test instance when it boots")
	pauseBeforeDestroy := flags.Bool("pause-before-destroy", false, "Prompt and wait before destroying the test instance")
	if ok, code := cmd.ParseFlags(flags, prog, args, "", stderr); !ok {
		return code
	}
	logger := ctxlog.New(stderr, "text", "info")
	defer func() {
		if err != nil {
			logger.WithError(err).Error("fatal")
			// suppress output from the other error-printing func
			err = nil
		}
		logger.Info("exiting")
	}()

	loader := config.NewLoader(stdin, logger)
	loader.Path = *configFile
	cfg, err := loader.Load()
	if err != nil {
		return 1
	}
	cluster, err := cfg.GetCluster("")
	if err != nil {
		return 1
	}
	key, err := config.LoadSSHKey(cluster.Containers.DispatchPrivateKey)
	if err != nil {
		err = fmt.Errorf("error loading Containers.DispatchPrivateKey: %s", err)
		return 1
	}
	driver, ok := dispatchcloud.Drivers[cluster.Containers.CloudVMs.Driver]
	if !ok {
		err = fmt.Errorf("unsupported cloud driver %q", cluster.Containers.CloudVMs.Driver)
		return 1
	}
	if *imageID == "" {
		*imageID = cluster.Containers.CloudVMs.ImageID
	}
	it, err := chooseInstanceType(cluster, *instanceType)
	if err != nil {
		return 1
	}
	tags := cloud.SharedResourceTags(cluster.Containers.CloudVMs.ResourceTags)
	tagKeyPrefix := cluster.Containers.CloudVMs.TagKeyPrefix
	tags[tagKeyPrefix+"CloudTestPID"] = fmt.Sprintf("%d", os.Getpid())
	if !(&tester{
		Logger:              logger,
		Tags:                tags,
		TagKeyPrefix:        tagKeyPrefix,
		SetID:               cloud.InstanceSetID(*instanceSetID),
		DestroyExisting:     *destroyExisting,
		ProbeInterval:       cluster.Containers.CloudVMs.ProbeInterval.Duration(),
		SyncInterval:        cluster.Containers.CloudVMs.SyncInterval.Duration(),
		TimeoutBooting:      cluster.Containers.CloudVMs.TimeoutBooting.Duration(),
		Driver:              driver,
		DriverParameters:    cluster.Containers.CloudVMs.DriverParameters,
		ImageID:             cloud.ImageID(*imageID),
		InstanceType:        it,
		SSHKey:              key,
		SSHPort:             cluster.Containers.CloudVMs.SSHPort,
		DeployPublicKey:     cluster.Containers.CloudVMs.DeployPublicKey,
		BootProbeCommand:    cluster.Containers.CloudVMs.BootProbeCommand,
		InstanceInitCommand: cloud.InitCommand(cluster.Containers.CloudVMs.InstanceInitCommand),
		ShellCommand:        *shellCommand,
		PauseBeforeDestroy: func() {
			if *pauseBeforeDestroy {
				logger.Info("waiting for operator to press Enter")
				fmt.Fprint(stderr, "Press Enter to continue: ")
				bufio.NewReader(stdin).ReadString('\n')
			}
		},
	}).Run() {
		return 1
	}
	return 0
}

// Return the named instance type, or the cheapest type if name=="".
func chooseInstanceType(cluster *arvados.Cluster, name string) (arvados.InstanceType, error) {
	if len(cluster.InstanceTypes) == 0 {
		return arvados.InstanceType{}, errors.New("no instance types are configured")
	} else if name == "" {
		first := true
		var best arvados.InstanceType
		for _, it := range cluster.InstanceTypes {
			if first || best.Price > it.Price {
				best = it
				first = false
			}
		}
		return best, nil
	} else if it, ok := cluster.InstanceTypes[name]; !ok {
		return it, fmt.Errorf("requested instance type %q is not configured", name)
	} else {
		return it, nil
	}
}
