// Copyright 2017 PingCAP, Inc.
//
// 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,
// See the License for the specific language governing permissions and
// limitations under the License.package spec

package bootstrap

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

	"github.com/juju/errors"
	"github.com/pingcap/tidb-operator/pkg/apis/pingcap.com/v1"
	"github.com/pingcap/tidb-operator/pkg/client/clientset/versioned"
	"github.com/pingcap/tidb-operator/pkg/crd"
	"github.com/pingcap/tidb-operator/pkg/scheduler/volume"
	"github.com/pingcap/tidb-operator/pkg/util/label"
	"github.com/pingcap/tidb-operator/pkg/volumeprovisioner"
	"github.com/pingcap/tidb-operator/test/e2e/util"
	yaml "gopkg.in/yaml.v2"
	corev1 "k8s.io/api/core/v1"
	apierrs "k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/labels"
	"k8s.io/apimachinery/pkg/util/wait"
	"k8s.io/client-go/kubernetes"
	"k8s.io/client-go/rest"
)

// OperatorImage is the image name of tidb-operator
var OperatorImage string

const (
	// OperatorNamespace is the namespace of tidb-operator
	OperatorNamespace = "pingcap"

	// VolumeConfigMapName is the name of tidb-volume ConfigMap
	VolumeConfigMapName = "tidb-volume"
)

// Bootstrap is responsible for starting operator
type Bootstrap struct {
	Cli     versioned.Interface
	KubeCli kubernetes.Interface
}

// New returns a Bootstrap
func New() (*Bootstrap, error) {
	cfg, err := rest.InClusterConfig()
	if err != nil {
		return nil, errors.Trace(err)
	}

	cfg.QPS = 1000
	cfg.Burst = 1000

	cli, err := versioned.NewForConfig(cfg)
	if err != nil {
		return nil, errors.Trace(err)
	}

	kubeCli, err := kubernetes.NewForConfig(cfg)
	if err != nil {
		return nil, errors.Trace(err)
	}

	return &Bootstrap{cli, kubeCli}, nil
}

// ResetE2E resets.
func (b *Bootstrap) ResetE2E(ns []string) error {
	for _, n := range ns {
		data, err := exec.Command("helm", "list", "--all", fmt.Sprintf("--namespace=%s", n), "--short").CombinedOutput()
		if err != nil {
			util.Logf("failed to list charts in %s %s: %v", n, string(data), err)
			continue
		}
		charts := strings.TrimSpace(string(data))
		if charts != "" {
			chartList := strings.Split(charts, "\n")
			for _, chart := range chartList {
				data, err = exec.Command("helm", "delete", chart, "--purge").CombinedOutput()
				if err != nil {
					util.Logf("failed to delete chart %s %s: %v", chart, data, err)
					return errors.Trace(err)
				}
			}
		}

		err = b.KubeCli.CoreV1().Namespaces().Delete(n, nil)
		if err != nil && !apierrs.IsNotFound(err) {
			util.Logf("failed to delete e2e namespace %s: %v", n, err)
			return errors.Trace(err)
		}
		err = wait.Poll(5*time.Second, 5*time.Minute, func() (bool, error) {
			_, err := b.KubeCli.CoreV1().Namespaces().Get(n, metav1.GetOptions{})
			if err == nil || !apierrs.IsNotFound(err) {
				return false, nil
			}
			return true, nil
		})
		if err != nil {
			util.Logf("timeout waiting for namespace %s deleted", n)
			return errors.Trace(err)
		}
	}
	return nil
}

// DeleteOperator deletes the tidb-operator helm package
func (b *Bootstrap) DeleteOperator() error {
	data, err := exec.Command("helm", "delete", "tidb-operator", "--purge").CombinedOutput()
	if err != nil {
		return errors.Errorf("Error when delete tidb-operator: %v, %s", err, string(data))
	}

	return nil
}

// WaitOperatorDeleted waits for the tidb-operator namespace to be delted.
func (b *Bootstrap) WaitOperatorDeleted() error {
	err := b.KubeCli.CoreV1().Namespaces().Delete(OperatorNamespace, nil)
	if err != nil {
		util.Logf("Error when delete tidb-operator namespace %s: %v", OperatorNamespace, err)
		return nil
	}

	return wait.Poll(5*time.Second, 10*time.Minute, func() (bool, error) {
		_, err := b.KubeCli.CoreV1().Namespaces().Get(OperatorNamespace, metav1.GetOptions{})
		if err != nil && apierrs.IsNotFound(err) {
			return true, nil
		}

		return false, nil
	})
}

// ApplyOperator installs the TiDB operator.
func (b *Bootstrap) ApplyOperator() error {
	operatorImage := os.Getenv("OPERATOR_IMAGE")
	return wait.Poll(5*time.Second, 10*time.Minute, func() (bool, error) {
		cmd := fmt.Sprintf("helm install /tmp/tidb-operator --namespace=%s --name=tidb-operator --values=/tmp/values.yaml --set operatorImage=%s", OperatorNamespace, operatorImage)
		data, err := exec.Command("/bin/sh", "-c", cmd).CombinedOutput()
		if err != nil {
			util.Logf("Error when apply tidb-operator: %v, %s", err, string(data))
			return false, nil
		}
		return true, nil
	})
}

// WaitOperatorReady waits for TiDB operator pods to be running.
func (b *Bootstrap) WaitOperatorReady() error {
	opts := metav1.ListOptions{
		LabelSelector: labels.SelectorFromSet(
			map[string]string{"app": "tidb-operator"},
		).String()}

	err := wait.Poll(5*time.Second, 10*time.Minute, func() (bool, error) {
		podList, err := b.KubeCli.CoreV1().Pods(OperatorNamespace).List(opts)
		if err != nil {
			return false, nil
		}
		pods := make(map[string]int)
		for _, pod := range podList.Items {
			name := pod.Labels["name"]
			if name == "" || pod.Status.Phase != corev1.PodRunning {
				continue
			}
			pods[name]++
		}
		if pods["tidb-controller-manager"] == 0 || pods["tidb-scheduler"] == 0 || pods["tidb-volume-manager"] == 0 {
			return false, nil
		}
		return true, nil
	})
	if err != nil {
		return errors.Trace(err)
	}

	clientset, err := crd.Client("")
	if err != nil {
		return errors.Trace(err)
	}

	err = crd.WaitForCRDReady(clientset, fmt.Sprintf("%s.%s", v1.TidbClusterResourcePlural, v1.SchemeGroupVersion.Group))
	if err != nil {
		return errors.Trace(err)
	}

	return crd.WaitForCRDReady(clientset, fmt.Sprintf("%s.%s", v1.TidbSetResourcePlural, v1.SchemeGroupVersion.Group))
}

// Teardown does nothing now
func (b *Bootstrap) Teardown() error {
	return nil
}

func (b *Bootstrap) nodes() ([]string, error) {
	var nodes []string
	nodeList, err := b.KubeCli.CoreV1().Nodes().List(metav1.ListOptions{})
	if err != nil {
		return nil, errors.Trace(err)
	}

	for _, node := range nodeList.Items {
		if node.Spec.Unschedulable {
			continue
		}

		nodes = append(nodes, node.Name)
	}

	return nodes, nil
}

// SetupTidbVolumeConfigMap configures test volumes.
func (b *Bootstrap) SetupTidbVolumeConfigMap() error {
	nodes, err := b.nodes()
	if err != nil {
		return err
	}

	var tidbVolumes volume.TiDBVolume
	for _, node := range nodes {
		tidbVolumes.Nodes = append(tidbVolumes.Nodes, volume.Node{
			Name: node,
			Directories: []volume.Directory{
				{Path: "/tidb/pd-dir-1", Size: "1Gi"},
				{Path: "/tidb/pd-dir-2", Size: "2Gi"},
				{Path: "/tidb/pd-dir-3", Size: "2.5Gi"},
				{Path: "/tidb/pd-dir-4", Size: "1.5Gi"},
				{Path: "/tidb/pd-dir-5", Size: "3Gi"},

				{Path: "/tidb/tikv-dir-1", Size: "100Gi"},
				{Path: "/tidb/tikv-dir-2", Size: "80Gi"},
				{Path: "/tidb/tikv-dir-3", Size: "30Gi"},
				{Path: "/tidb/tikv-dir-4", Size: "100Gi"},
				{Path: "/tidb/tikv-dir-5", Size: "100Gi"},
				{Path: "/tidb/tikv-dir-6", Size: "100Gi"},
				{Path: "/tidb/tikv-dir-7", Size: "100Gi"},
				{Path: "/tidb/tikv-dir-8", Size: "100Gi"},
				{Path: "/tidb/tikv-dir-9", Size: "100Gi"},
				{Path: "/tidb/tikv-dir-10", Size: "100Gi"},
			},
		})
	}

	data, err := yaml.Marshal(tidbVolumes)
	if err != nil {
		return errors.Trace(err)
	}
	dataStr := string(data)

	cm, err := b.KubeCli.CoreV1().ConfigMaps(OperatorNamespace).Get(VolumeConfigMapName, metav1.GetOptions{})
	if err != nil {
		if !apierrs.IsNotFound(err) {
			return errors.Trace(err)
		}

		cm = &corev1.ConfigMap{
			ObjectMeta: metav1.ObjectMeta{
				Name:      VolumeConfigMapName,
				Namespace: OperatorNamespace,
			},
			Data: map[string]string{"volume": dataStr},
		}

		_, err := b.KubeCli.CoreV1().ConfigMaps(OperatorNamespace).Create(cm)
		if err != nil {
			return errors.Trace(err)
		}

		return nil
	}

	cm.Data = map[string]string{"volume": dataStr}
	_, err = b.KubeCli.CoreV1().ConfigMaps(OperatorNamespace).Update(cm)
	if err != nil {
		return errors.Trace(err)
	}

	return nil
}

// ClearPVs waits for persitent volumes to be deleted after first annotating them for deletion.
func (b *Bootstrap) ClearPVs() error {
	opts := metav1.ListOptions{
		LabelSelector: labels.SelectorFromSet(
			label.New().Labels(),
		).String(),
	}

	err := wait.Poll(5*time.Second, 5*time.Minute, func() (bool, error) {
		pvList, err := b.KubeCli.CoreV1().PersistentVolumes().List(opts)
		if err != nil {
			util.Logf("%v", err)
			return false, nil
		}

		for _, pv := range pvList.Items {
			pv.Annotations[volumeprovisioner.AnnPVManualDeleted] = time.Now().Format(time.RFC3339)
			pv.Annotations[volumeprovisioner.AnnPVTombstoned] = time.Now().Format(time.RFC3339)
			_, err = b.KubeCli.CoreV1().PersistentVolumes().Update(&pv)
			if err != nil {
				util.Logf("%v", err)
				return false, nil
			}
		}

		return true, nil
	})

	if err != nil {
		return errors.Trace(err)
	}

	return wait.Poll(5*time.Second, 5*time.Minute, func() (bool, error) {
		pvList, err := b.KubeCli.CoreV1().PersistentVolumes().List(opts)
		if err != nil {
			util.Logf("%v", err)
			return false, nil
		}

		if len(pvList.Items) != 0 {
			util.Logf("pvCount = %d", len(pvList.Items))
			return false, nil
		}

		return true, nil
	})
}
