/*
Copyright 2019 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 create

import (
	"fmt"
	"math/rand"
	"regexp"
	"time"

	"github.com/alessio/shellescape"

	"sigs.k8s.io/kind/pkg/cluster/internal/delete"
	"sigs.k8s.io/kind/pkg/cluster/internal/providers"
	"sigs.k8s.io/kind/pkg/errors"
	"sigs.k8s.io/kind/pkg/internal/apis/config"
	"sigs.k8s.io/kind/pkg/internal/apis/config/encoding"
	"sigs.k8s.io/kind/pkg/internal/cli"
	"sigs.k8s.io/kind/pkg/log"

	"sigs.k8s.io/kind/pkg/cluster/internal/create/actions"
	configaction "sigs.k8s.io/kind/pkg/cluster/internal/create/actions/config"
	"sigs.k8s.io/kind/pkg/cluster/internal/create/actions/installcni"
	"sigs.k8s.io/kind/pkg/cluster/internal/create/actions/installstorage"
	"sigs.k8s.io/kind/pkg/cluster/internal/create/actions/kubeadminit"
	"sigs.k8s.io/kind/pkg/cluster/internal/create/actions/kubeadmjoin"
	"sigs.k8s.io/kind/pkg/cluster/internal/create/actions/loadbalancer"
	"sigs.k8s.io/kind/pkg/cluster/internal/create/actions/waitforready"
	"sigs.k8s.io/kind/pkg/cluster/internal/kubeconfig"
)

const (
	// Typical host name max limit is 64 characters (https://linux.die.net/man/2/sethostname)
	// We append -control-plane (14 characters) to the cluster name on the control plane container
	clusterNameMax = 50
)

// similar to valid docker container names, but since we will prefix
// and suffix this name, we can relax it a little
// see NewContext() for usage
// https://godoc.org/github.com/docker/docker/daemon/names#pkg-constants
var validNameRE = regexp.MustCompile(`^[a-z0-9_.-]+$`)

// ClusterOptions holds cluster creation options
type ClusterOptions struct {
	Config       *config.Cluster
	NameOverride string // overrides config.Name
	// NodeImage overrides the nodes' images in Config if non-zero
	NodeImage      string
	Retain         bool
	WaitForReady   time.Duration
	KubeconfigPath string
	// see https://github.com/kubernetes-sigs/kind/issues/324
	StopBeforeSettingUpKubernetes bool // if false kind should setup kubernetes after creating nodes
	// Options to control output
	DisplayUsage      bool
	DisplaySalutation bool
}

// Cluster creates a cluster
func Cluster(logger log.Logger, p providers.Provider, opts *ClusterOptions) error {
	// default / process options (namely config)
	if err := fixupOptions(opts); err != nil {
		return err
	}

	// Check if the cluster name already exists
	if err := alreadyExists(p, opts.Config.Name); err != nil {
		return err
	}

	// TODO: move to config validation
	// validate the name
	if !validNameRE.MatchString(opts.Config.Name) {
		return errors.Errorf(
			"'%s' is not a valid cluster name, cluster names must match `%s`",
			opts.Config.Name, validNameRE.String(),
		)
	}
	// warn if cluster name might typically be too long
	if len(opts.Config.Name) > clusterNameMax {
		logger.Warnf("cluster name %q is probably too long, this might not work properly on some systems", opts.Config.Name)
	}

	// then validate
	if err := opts.Config.Validate(); err != nil {
		return err
	}

	// setup a status object to show progress to the user
	status := cli.StatusForLogger(logger)

	// we're going to start creating now, tell the user
	logger.V(0).Infof("Creating cluster %q ...\n", opts.Config.Name)

	// Create node containers implementing defined config Nodes
	if err := p.Provision(status, opts.Config); err != nil {
		// In case of errors nodes are deleted (except if retain is explicitly set)
		if !opts.Retain {
			_ = delete.Cluster(logger, p, opts.Config.Name, opts.KubeconfigPath)
		}
		return err
	}

	// TODO(bentheelder): make this controllable from the command line?
	actionsToRun := []actions.Action{
		loadbalancer.NewAction(), // setup external loadbalancer
		configaction.NewAction(), // setup kubeadm config
	}
	if !opts.StopBeforeSettingUpKubernetes {
		actionsToRun = append(actionsToRun,
			kubeadminit.NewAction(), // run kubeadm init
		)
		// this step might be skipped, but is next after init
		if !opts.Config.Networking.DisableDefaultCNI {
			actionsToRun = append(actionsToRun,
				installcni.NewAction(), // install CNI
			)
		}
		// add remaining steps
		actionsToRun = append(actionsToRun,
			installstorage.NewAction(),                // install StorageClass
			kubeadmjoin.NewAction(),                   // run kubeadm join
			waitforready.NewAction(opts.WaitForReady), // wait for cluster readiness
		)
	}

	// run all actions
	actionsContext := actions.NewActionContext(logger, status, p, opts.Config)
	for _, action := range actionsToRun {
		if err := action.Execute(actionsContext); err != nil {
			if !opts.Retain {
				_ = delete.Cluster(logger, p, opts.Config.Name, opts.KubeconfigPath)
			}
			return err
		}
	}

	// skip the rest if we're not setting up kubernetes
	if opts.StopBeforeSettingUpKubernetes {
		return nil
	}

	// try exporting kubeconfig with backoff for locking failures
	// TODO: factor out into a public errors API w/ backoff handling?
	// for now this is easier than coming up with a good API
	var err error
	for _, b := range []time.Duration{0, time.Millisecond, time.Millisecond * 50, time.Millisecond * 100} {
		time.Sleep(b)
		if err = kubeconfig.Export(p, opts.Config.Name, opts.KubeconfigPath); err == nil {
			break
		}
	}
	if err != nil {
		return err
	}

	// optionally display usage
	if opts.DisplayUsage {
		logUsage(logger, opts.Config.Name, opts.KubeconfigPath)
	}
	// optionally give the user a friendly salutation
	if opts.DisplaySalutation {
		logger.V(0).Info("")
		logSalutation(logger)
	}
	return nil
}

// alreadyExists returns an error if the cluster name already exists
// or if we had an error checking
func alreadyExists(p providers.Provider, name string) error {
	n, err := p.ListNodes(name)
	if err != nil {
		return err
	}
	if len(n) != 0 {
		return errors.Errorf("node(s) already exist for a cluster with the name %q", name)
	}
	return nil
}

func logUsage(logger log.Logger, name, explicitKubeconfigPath string) {
	// construct a sample command for interacting with the cluster
	kctx := kubeconfig.ContextForCluster(name)
	sampleCommand := fmt.Sprintf("kubectl cluster-info --context %s", kctx)
	if explicitKubeconfigPath != "" {
		// explicit path, include this
		sampleCommand += " --kubeconfig " + shellescape.Quote(explicitKubeconfigPath)
	}
	logger.V(0).Infof(`Set kubectl context to "%s"`, kctx)
	logger.V(0).Infof("You can now use your cluster with:\n\n" + sampleCommand)
}

func logSalutation(logger log.Logger) {
	salutations := []string{
		"Have a nice day! 👋",
		"Thanks for using kind! 😊",
		"Not sure what to do next? 😅  Check out https://kind.sigs.k8s.io/docs/user/quick-start/",
		"Have a question, bug, or feature request? Let us know! https://kind.sigs.k8s.io/#community 🙂",
	}
	r := rand.New(rand.NewSource(time.Now().UTC().UnixNano()))
	s := salutations[r.Intn(len(salutations))]
	logger.V(0).Info(s)
}

func fixupOptions(opts *ClusterOptions) error {
	// do post processing for options
	// first ensure we at least have a default cluster config
	if opts.Config == nil {
		cfg, err := encoding.Load("")
		if err != nil {
			return err
		}
		opts.Config = cfg
	}

	if opts.NameOverride != "" {
		opts.Config.Name = opts.NameOverride
	}

	// if NodeImage was set, override the image on all nodes
	if opts.NodeImage != "" {
		// Apply image override to all the Nodes defined in Config
		// TODO(fabrizio pandini): this should be reconsidered when implementing
		//     https://github.com/kubernetes-sigs/kind/issues/133
		for i := range opts.Config.Nodes {
			opts.Config.Nodes[i].Image = opts.NodeImage
		}
	}

	// default config fields (important for usage as a library, where the config
	// may be constructed in memory rather than from disk)
	config.SetDefaultsCluster(opts.Config)

	return nil
}
