package k8s

import (
	"bytes"
	"context"
	"io/ioutil"
	"os"
	"path"
	"strings"
	"text/template"
	"time"

	_ "github.com/go-sql-driver/mysql" // for connect tidb
	"github.com/juju/errors"
	"github.com/ngaut/log"
	"github.com/pingcap/schrodinger/pkg/types"
	"github.com/pingcap/schrodinger/util"
	"github.com/pingcap/tidb-operator/pkg/client/clientset/versioned"
	"k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/labels"
	"k8s.io/client-go/kubernetes"
	"k8s.io/client-go/util/retry"
	"k8s.io/helm/pkg/helm"
)

const (
	// TiDBClusterNameSuffix defines the name of tidb cluster
	TiDBClusterNameSuffix = "tidb-cluster"
	tidbChartDir          = "tidb-cluster"
	templateFile          = "values.yaml"
	maxWaitTimeout        = 30 * time.Minute
	checkInterval         = 20 * time.Second
)

// ClusterController manages tidb cluster
type ClusterController struct {
	K8sCli     kubernetes.Interface
	ClusterCli versioned.Interface
	HelmCli    helm.Interface
	// Namespace is namespace for client to retrieve configmap for pods, it will be "pingcap"
	Namespace        string
	ClusterAgentPort int
	TiDBChartPath    string
	TemplateFile     string
}

// NewClusterController returns a client that manages tidb cluster on k8s
func NewClusterController(
	K8sCli kubernetes.Interface,
	ClusterCli versioned.Interface,
	helmCli helm.Interface,
	namespace string,
	clusterAgentPort int,
	chartPath string,
) *ClusterController {
	return &ClusterController{
		K8sCli:           K8sCli,
		ClusterCli:       ClusterCli,
		HelmCli:          helmCli,
		Namespace:        namespace,
		ClusterAgentPort: clusterAgentPort,
		TiDBChartPath:    path.Join(chartPath, tidbChartDir),
		TemplateFile:     path.Join(chartPath, templateFile),
	}
}

// CreateCluster creates a tidb cluster on k8s
func (c *ClusterController) CreateCluster(ctx context.Context, cluster *types.Cluster, waitOk bool) error {
	tf, err := os.Open(c.TemplateFile)
	defer tf.Close()
	if err != nil {
		log.Error(err)
		return errors.Trace(err)
	}
	templ, err := ioutil.ReadAll(tf)
	if err != nil {
		log.Error(err)
		return errors.Trace(err)
	}
	tmpl, err := template.New("values").Delims("[[", "]]").Funcs(funcMap).Parse(string(templ))
	if err != nil {
		log.Error(err)
		return errors.Trace(err)
	}
	// log.Infof("%+v", cluster)
	vals := bytes.NewBuffer([]byte{})
	if err = tmpl.Execute(vals, cluster); err != nil {
		log.Error(err)
		return errors.Trace(err)
	}

	// return nil
	_, err = c.HelmCli.InstallRelease(
		c.TiDBChartPath,
		cluster.Namespace,
		helm.ValueOverrides(vals.Bytes()),
		// helm.ReleaseName(fmt.Sprintf("%s-%s", cluster.Namespace, cluster.Name)),
		helm.ReleaseName(cluster.Namespace),
	)
	if err != nil {
		return errors.Trace(err)
	}

	return errors.Trace(c.getClusterAccessInfo(ctx, cluster, waitOk))
}

// UpdateCluster updates a tidb cluster on k8s
func (c *ClusterController) UpdateCluster(ctx context.Context, cluster *types.Cluster, waitOk bool) error {
	tf, err := os.Open(c.TemplateFile)
	defer tf.Close()
	if err != nil {
		return errors.Trace(err)
	}
	templ, err := ioutil.ReadAll(tf)
	if err != nil {
		return errors.Trace(err)
	}
	tmpl, err := template.New("values").Delims("[[", "]]").Funcs(funcMap).Parse(string(templ))
	if err != nil {
		return errors.Trace(err)
	}
	vals := bytes.NewBuffer([]byte{})
	if err = tmpl.Execute(vals, cluster); err != nil {
		return errors.Trace(err)
	}

	_, err = c.HelmCli.UpdateRelease(
		cluster.Namespace,
		c.TiDBChartPath,
		helm.UpdateValueOverrides(vals.Bytes()),
	)
	if err != nil {
		return errors.Trace(err)
	}

	return errors.Trace(c.getClusterAccessInfo(ctx, cluster, waitOk))
}

// ListCluster lists all clusters
func (c *ClusterController) ListCluster(spec *types.Cluster) ([]*types.Cluster, error) {
	tcs, err := c.ClusterCli.PingcapV1alpha1().TidbClusters(v1.NamespaceAll).List(metav1.ListOptions{})
	if err != nil {
		return nil, errors.Errorf("list all cluster failed: %v", err)
	}
	var clusters []*types.Cluster
	for _, clu := range tcs.Items {
		cluster := &types.Cluster{
			TidbCluster: &clu,
		}
		clusters = append(clusters, cluster)
	}

	return clusters, nil
}

// QueryCluster queries cluster by release name
func (c *ClusterController) QueryCluster(ctx context.Context, name string, waitOk bool) (*types.Cluster, error) {
	// ns := getNamespace(name)
	tc, err := c.ClusterCli.PingcapV1alpha1().TidbClusters(name).Get(TiDBClusterNameSuffix, metav1.GetOptions{})
	if err != nil {
		return nil, errors.Trace(err)
	}
	cluster := &types.Cluster{
		TidbCluster: tc,
	}

	if err = c.getClusterAccessInfo(ctx, cluster, waitOk); err != nil {
		log.Errorf("get cluster info failed: %v", err)
		c.setClusterStatus(ctx, cluster)
		return cluster, errors.Trace(err)
	}

	c.setClusterStatus(ctx, cluster)

	return cluster, nil
}

// DeleteCluster deletes cluster by release name
func (c *ClusterController) DeleteCluster(name string) error {
	opts := []helm.DeleteOption{
		helm.DeletePurge(true),
	}
	_, err := c.HelmCli.DeleteRelease(name, opts...)
	if err != nil {
		return errors.Trace(err)
	}

	// delete all pvc
	// TODO: use label to delete pvc
	if err = c.K8sCli.CoreV1().PersistentVolumeClaims(name).
		DeleteCollection(&metav1.DeleteOptions{}, metav1.ListOptions{}); err != nil {
		return errors.Trace(err)
	}

	return errors.Trace(util.DeleteK8sNamespace(name, c.K8sCli))
}

// BackupCluster backups cluster
func (c *ClusterController) BackupCluster(ctx context.Context, tidbClusterBackup *types.TiDBClusterBackup) error {
	log.Infof("namespace %s, start to backup cluster", tidbClusterBackup.Namespace)
	if err := c.backupCluster(ctx, tidbClusterBackup); err != nil {
		log.Errorf("namespace: %s, backup cluster failed: %v", tidbClusterBackup.Namespace, err)
		return errors.Trace(err)
	}
	log.Infof("namespace: %s, backup cluster end", tidbClusterBackup.Namespace)
	return nil
}

// RestoreCluster restores tidb cluster
func (c *ClusterController) RestoreCluster(ctx context.Context, tidbClusterBackup *types.TiDBClusterBackup) error {
	log.Infof("namespace %s, start to restore cluster", tidbClusterBackup.Namespace)
	if err := c.recoverCluster(ctx, tidbClusterBackup); err != nil {
		log.Errorf("namespace: %s, restore failed: %v", tidbClusterBackup.Namespace, err)
		return errors.Trace(err)
	}
	log.Infof("namespace: %s, restore cluster end", tidbClusterBackup.Namespace)
	return nil
}

// GetNodesByLabel returns nodes with label
func (c *ClusterController) GetNodesByLabel(labelsMap map[string]string) ([]string, error) {
	listOptions := metav1.ListOptions{
		LabelSelector: labels.SelectorFromSet(labelsMap).String(),
	}
	nodes, err := c.K8sCli.CoreV1().Nodes().List(listOptions)
	if err != nil {
		return nil, errors.Trace(err)
	}

	var nodesArr []string
	for _, node := range nodes.Items {
		nodesArr = append(nodesArr, node.Name)
	}
	return nodesArr, nil
}

// SetNodeLabels sets labels for node
func (c *ClusterController) SetNodeLabels(nodeName string, labels map[string]string) error {
	return errors.Trace(retry.RetryOnConflict(retry.DefaultRetry, func() error {
		node, err := c.K8sCli.CoreV1().Nodes().Get(nodeName, metav1.GetOptions{})
		if err != nil {
			return errors.Trace(err)
		}

		for key, val := range labels {
			node.Labels[strings.TrimSpace(key)] = strings.TrimSpace(val)
		}

		if _, err = c.K8sCli.CoreV1().Nodes().Update(node); err != nil {
			log.Errorf("set labels %v to node %s failed: %v", labels, nodeName, err)
			return err
		}

		log.Infof("set labels %v for node %s successfully", labels, nodeName)
		return nil
	}))
}

// DeleteNodeLabel deletes the label of the node
func (c *ClusterController) DeleteNodeLabel(nodeName string, label string) error {
	return c.deleteLabelFromNode(nodeName, label)
}

func (c *ClusterController) deleteLabelFromNode(nodeName string, label string) error {
	return errors.Trace(retry.RetryOnConflict(retry.DefaultRetry, func() error {
		node, err := c.K8sCli.CoreV1().Nodes().Get(nodeName, metav1.GetOptions{})
		if err != nil {
			return errors.Trace(err)
		}

		delete(node.Labels, label)

		if _, err = c.K8sCli.CoreV1().Nodes().Update(node); err != nil {
			log.Errorf("delete label %s from node %s failed: %v", label, nodeName, err)
			return err
		}

		log.Infof("delete label %s from node %s successfully", label, nodeName)
		return nil
	}))
}

// ListNodes lists all nodes of the cluster
func (c *ClusterController) ListNodes() ([]string, error) {
	var nodes []string
	nodeList, err := c.K8sCli.CoreV1().Nodes().List(metav1.ListOptions{})
	if err != nil {
		return nodes, errors.Trace(err)
	}

	for _, node := range nodeList.Items {
		nodes = append(nodes, node.Name)
	}

	return nodes, nil
}

// CleanLabelFromNodes cleans the label from all nodes
func (c *ClusterController) CleanLabelFromNodes(label string) error {
	nodeList, err := c.K8sCli.CoreV1().Nodes().List(metav1.ListOptions{})
	if err != nil {
		return errors.Trace(err)
	}

	for _, n := range nodeList.Items {
		if err = c.deleteLabelFromNode(n.Name, label); err != nil {
			log.Error(err)
			return errors.Trace(err)
		}
	}

	return nil
}
