// 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 controller

import (
	"fmt"

	"github.com/juju/errors"
	"github.com/ngaut/log"
	"github.com/pingcap/tidb-operator/pkg/apis/pingcap.com/v1"
	"github.com/pingcap/tidb-operator/pkg/util"
	"github.com/pingcap/tidb-operator/pkg/util/label"
	appsv1beta1 "k8s.io/api/apps/v1beta1"
	batchv1 "k8s.io/api/batch/v1"
	corev1 "k8s.io/api/core/v1"
	rbacv1beta1 "k8s.io/api/rbac/v1beta1"
	apierrors "k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/util/intstr"
)

const (
	defaultPromRetentionDays = 15
)

func (tcc *TiDBClusterController) syncMonitorFor(cluster *v1.TidbCluster) error {
	ns := cluster.GetNamespace()
	clusterName := cluster.GetName()
	if RBACEnabled {
		if err := tcc.ensureRBAC(ns, fmt.Sprintf("%s-tidb-prometheus", clusterName)); err != nil {
			log.Errorf("failed to sync RBAC settings in [%s/%s]: %v", ns, clusterName, err)
			return errors.Trace(err)
		}
	}
	if err := tcc.syncMonitorJob(cluster); err != nil {
		log.Errorf("failed to sync configure monitor job in [%s/%s]: %v", ns, clusterName, err)
		return errors.Trace(err)
	}
	services := map[string]int{
		"prometheus": 9090,
		"grafana":    3000,
	}
	for kind, port := range services {
		if err := tcc.syncMonitorService(cluster, kind, port); err != nil {
			log.Errorf("failed to sync monitor service in [%s/%s]: %v", ns, clusterName, err)
			return errors.Trace(err)
		}
	}
	if err := tcc.ensureMonitorDeploymentExist(cluster); err != nil {
		log.Errorf("failed to ensure monitor deployment exist in [%s/%s]: %v", ns, clusterName, err)
		return errors.Trace(err)
	}
	return nil
}

func (tcc *TiDBClusterController) syncMonitorJob(cluster *v1.TidbCluster) error {
	ns := cluster.GetNamespace()
	clusterName := cluster.GetName()

	jobName := fmt.Sprintf("%s-configure-grafana", clusterName)
	jobLabel := label.New().Cluster(clusterName).App("configure-grafana").Labels()
	_, err := tcc.kubeCli.BatchV1().Jobs(ns).Get(jobName, metav1.GetOptions{})
	if err == nil {
		log.Infof("job [%s/%s] already exist,doesn't need to create.", ns, jobName)
		return nil
	}

	if !apierrors.IsNotFound(err) {
		log.Errorf("failed to get job %s/%s: %v", ns, jobName, err)
		return errors.Trace(err)
	}
	secretName := secretNameFor(cluster)
	job := &batchv1.Job{
		ObjectMeta: metav1.ObjectMeta{
			Name:            jobName,
			Labels:          jobLabel,
			OwnerReferences: []metav1.OwnerReference{getOwnerRef(cluster)},
		},
		Spec: batchv1.JobSpec{
			Template: corev1.PodTemplateSpec{
				ObjectMeta: metav1.ObjectMeta{
					Name: jobName,
				},
				Spec: corev1.PodSpec{
					RestartPolicy: corev1.RestartPolicyOnFailure,
					Containers: []corev1.Container{
						{
							Name:  "tidb-dashboard-installer",
							Image: cluster.Spec.Monitor.DashboardInstaller.Image,
							Args:  []string{fmt.Sprintf("%s-grafana:3000", clusterName)},
							Env: []corev1.EnvVar{
								{Name: "GRAFANA_USERNAME", ValueFrom: &corev1.EnvVarSource{
									SecretKeyRef: &corev1.SecretKeySelector{
										LocalObjectReference: corev1.LocalObjectReference{Name: secretName}, Key: "grafana-username"},
								}},
								{Name: "GRAFANA_PASSWORD", ValueFrom: &corev1.EnvVarSource{
									SecretKeyRef: &corev1.SecretKeySelector{
										LocalObjectReference: corev1.LocalObjectReference{Name: secretName}, Key: "grafana-password"},
								}},
							},
						},
					},
				},
			},
		},
	}
	if _, err := tcc.kubeCli.BatchV1().Jobs(ns).Create(job); err != nil {
		log.Errorf("failed to create job %s/%s: %v", ns, jobName, err)
		return errors.Trace(err)
	}
	log.Infof("job [%s/%s] created success!", ns, job.Name)

	return nil
}

func (tcc *TiDBClusterController) syncMonitorService(cluster *v1.TidbCluster, kind string, port int) error {
	ns := cluster.GetNamespace()
	clusterName := cluster.GetName()

	svcName := fmt.Sprintf("%s-%s", clusterName, kind)
	_, err := tcc.kubeCli.CoreV1().Services(ns).Get(svcName, metav1.GetOptions{})
	if err == nil {
		log.Infof("service [%s/%s] already exist,doesn't need to create it.", ns, svcName)
		return nil
	}

	svcLabel := label.New().Cluster(clusterName).App("tidb-monitor").Labels()
	svc := &corev1.Service{
		ObjectMeta: metav1.ObjectMeta{
			Name:            svcName,
			Labels:          svcLabel,
			OwnerReferences: []metav1.OwnerReference{getOwnerRef(cluster)},
		},
		Spec: corev1.ServiceSpec{
			Type: getServiceType(cluster.Spec.Services, kind),
			Ports: []corev1.ServicePort{
				{
					Name:       kind,
					Port:       int32(port),
					TargetPort: intstr.FromInt(port),
					Protocol:   corev1.ProtocolTCP,
				},
			},
			Selector: svcLabel,
		},
	}
	if _, err := tcc.kubeCli.CoreV1().Services(ns).Create(svc); err != nil && !apierrors.IsAlreadyExists(err) {
		log.Errorf("failed to create monitor service %s/%s %v", ns, svcName, err)
		return errors.Trace(err)
	}
	log.Infof("monitor service %s/%s created successfully", ns, svcName)
	return nil
}

// TODO(tennix): update deployment when monitor image and size changes
func (tcc *TiDBClusterController) ensureMonitorDeploymentExist(cluster *v1.TidbCluster) error {
	ns := cluster.GetNamespace()
	clusterName := cluster.GetName()

	deploymentName := fmt.Sprintf("%s-monitor", clusterName)
	configName := cluster.GetConfigMapName()
	deploymentLabel := label.New().Cluster(clusterName).App("tidb-monitor").Labels()
	var reserveDays int32 = defaultPromRetentionDays
	if cluster.Spec.Monitor.RetentionDays > 0 {
		reserveDays = cluster.Spec.Monitor.RetentionDays
	}

	_, err := tcc.kubeCli.AppsV1beta1().Deployments(ns).Get(deploymentName, metav1.GetOptions{})
	if err == nil {
		return nil
	}

	if !apierrors.IsNotFound(err) {
		log.Errorf("failed to get TiDB monitor %s/%s: %v", ns, deploymentName, err)
		return err
	}

	cm, err := tcc.kubeCli.CoreV1().ConfigMaps(ns).Get(configName, metav1.GetOptions{})
	if err != nil {
		log.Errorf("failed to get ConfigMap %s/%s: %v", ns, configName, err)
		return err
	}

	prometheusVolumeMounts := []corev1.VolumeMount{
		{Name: "timezone", ReadOnly: true, MountPath: "/etc/localtime"},
		{Name: "prometheus-config", ReadOnly: true, MountPath: "/etc/prometheus"},
		{Name: "prometheus-data", MountPath: "/prometheus"},
	}

	grafanaVolumeMounts := []corev1.VolumeMount{
		{Name: "timezone", ReadOnly: true, MountPath: "/etc/localtime"},
		{Name: "grafana-data", MountPath: "/data"},
	}

	volumes := []corev1.Volume{
		{Name: "timezone", VolumeSource: corev1.VolumeSource{HostPath: &corev1.HostPathVolumeSource{Path: "/etc/localtime"}}},
		{Name: "prometheus-data", VolumeSource: corev1.VolumeSource{EmptyDir: &corev1.EmptyDirVolumeSource{}}},
		{Name: "grafana-data", VolumeSource: corev1.VolumeSource{EmptyDir: &corev1.EmptyDirVolumeSource{}}},
		{Name: "prometheus-config", VolumeSource: corev1.VolumeSource{
			ConfigMap: &corev1.ConfigMapVolumeSource{
				LocalObjectReference: corev1.LocalObjectReference{Name: configName},
				Items: []corev1.KeyToPath{
					{Key: "prometheus-config", Path: "prometheus.yml"},
					{Key: "alert-rules-config", Path: "alert.rules"},
				},
			},
		}},
	}

	if _, ok := cm.Data["grafana-config"]; ok {
		grafanaVolumeMounts = append(grafanaVolumeMounts, corev1.VolumeMount{Name: "grafana-config", MountPath: "/etc/grafana"})
		volumes = append(volumes, corev1.Volume{
			Name: "grafana-config", VolumeSource: corev1.VolumeSource{
				ConfigMap: &corev1.ConfigMapVolumeSource{
					LocalObjectReference: corev1.LocalObjectReference{Name: configName},
					Items: []corev1.KeyToPath{
						{Key: "grafana-config", Path: "grafana.ini"},
					},
				},
			},
		})
	}
	deploy := &appsv1beta1.Deployment{
		ObjectMeta: metav1.ObjectMeta{
			Name:            deploymentName,
			Namespace:       ns,
			Labels:          deploymentLabel,
			OwnerReferences: []metav1.OwnerReference{getOwnerRef(cluster)},
		},
		Spec: appsv1beta1.DeploymentSpec{
			Template: corev1.PodTemplateSpec{
				ObjectMeta: metav1.ObjectMeta{
					Name:   deploymentName,
					Labels: deploymentLabel,
				},
				Spec: corev1.PodSpec{
					Affinity: util.AffinityForNodeSelector(
						ns,
						cluster.Spec.Monitor.NodeSelectorRequired,
						label.New().Cluster(clusterName).App("monitor"),
						cluster.Spec.Monitor.NodeSelector,
					),
					ServiceAccountName: fmt.Sprintf("%s-tidb-prometheus", clusterName),
					Containers: []corev1.Container{
						{
							Name:  "prometheus",
							Image: cluster.Spec.Monitor.Prometheus.Image,
							Command: []string{
								"/bin/prometheus",
							},
							Args: []string{
								"--config.file=/etc/prometheus/prometheus.yml",
								fmt.Sprintf("--storage.tsdb.retention=%dd", reserveDays),
								"--storage.tsdb.path=/prometheus",
							},
							Ports: []corev1.ContainerPort{
								{
									Name:          "prometheus",
									ContainerPort: int32(9090),
									Protocol:      corev1.ProtocolTCP,
								},
							},
							VolumeMounts: prometheusVolumeMounts,
							Resources:    util.ResourceRequirement(cluster.Spec.Monitor.Prometheus),
						},
						{ // TODO: disable grafana if grafana is nil
							Name:  "grafana",
							Image: cluster.Spec.Monitor.Grafana.Image,
							Ports: []corev1.ContainerPort{
								{
									Name:          "grafana",
									ContainerPort: int32(3000),
									Protocol:      corev1.ProtocolTCP,
								},
							},
							VolumeMounts: grafanaVolumeMounts,
							Env: []corev1.EnvVar{
								{Name: "GF_PATHS_DATA", Value: "/data"},
							},
							Resources: util.ResourceRequirement(*cluster.Spec.Monitor.Grafana),
						},
					},
					Volumes: volumes,
				},
			},
		},
	}

	if _, err := tcc.kubeCli.AppsV1beta1().Deployments(ns).Create(deploy); err != nil {
		log.Errorf("failed to create monitor deployment %s/%s: %v", ns, deploymentName, err)
		return err
	}
	log.Infof("monitor deployment %s/%s created successfully", ns, deploymentName)
	return nil
}

func (tcc *TiDBClusterController) ensureRBAC(ns, name string) error {
	if err := tcc.ensureSAExists(ns, name); err != nil {
		return err
	}
	if err := tcc.ensureRoleExists(ns, name); err != nil {
		return err
	}
	return tcc.ensureRBExists(ns, name)
}

// ensure Role exists
func (tcc *TiDBClusterController) ensureRoleExists(ns, name string) error {
	_, err := tcc.kubeCli.RbacV1beta1().Roles(ns).Get(name, metav1.GetOptions{})
	if err != nil && !apierrors.IsNotFound(err) {
		return errors.Trace(err)
	}
	if err == nil {
		return nil
	}
	role := &rbacv1beta1.Role{
		ObjectMeta: metav1.ObjectMeta{
			Name:      name,
			Namespace: ns,
		},
		Rules: []rbacv1beta1.PolicyRule{
			{
				APIGroups: []string{""},
				Resources: []string{"pods"},
				Verbs:     []string{"get", "watch", "list"},
			},
		},
	}
	_, err = tcc.kubeCli.RbacV1beta1().Roles(ns).Create(role)
	if err != nil {
		log.Errorf("failed to create Role %s/%s: %v", ns, name, err)
		return errors.Trace(err)
	}
	log.Infof("successfully created Role %s/%s", ns, name)
	return nil
}

// ensure RoleBinding exists
func (tcc *TiDBClusterController) ensureRBExists(ns, name string) error {
	_, err := tcc.kubeCli.RbacV1beta1().RoleBindings(ns).Get(name, metav1.GetOptions{})
	if err != nil && !apierrors.IsNotFound(err) {
		return errors.Trace(err)
	}
	if err == nil {
		return nil
	}
	rb := &rbacv1beta1.RoleBinding{
		ObjectMeta: metav1.ObjectMeta{
			Name:      name,
			Namespace: ns,
		},
		Subjects: []rbacv1beta1.Subject{
			{
				Kind: rbacv1beta1.ServiceAccountKind,
				Name: name,
			},
		},
		RoleRef: rbacv1beta1.RoleRef{
			Kind:     "Role",
			Name:     name,
			APIGroup: rbacv1beta1.GroupName,
		},
	}
	_, err = tcc.kubeCli.RbacV1beta1().RoleBindings(ns).Create(rb)
	if err != nil {
		log.Errorf("failed to create RoleBinding %s/%s: %v", ns, name, err)
		return errors.Trace(err)
	}
	log.Infof("successfully created RoleBinding %s/%s", ns, name)
	return nil
}

// ensure ServiceAccount exists
func (tcc *TiDBClusterController) ensureSAExists(ns, name string) error {
	_, err := tcc.kubeCli.CoreV1().ServiceAccounts(ns).Get(name, metav1.GetOptions{})
	if err != nil && !apierrors.IsNotFound(err) {
		return errors.Trace(err)
	}
	if err == nil {
		return nil
	}
	sa := &corev1.ServiceAccount{
		ObjectMeta: metav1.ObjectMeta{
			Namespace: ns,
			Name:      name,
		},
	}
	_, err = tcc.kubeCli.CoreV1().ServiceAccounts(ns).Create(sa)
	if err != nil {
		log.Errorf("failed to create ServiceAccount %s/%s: %v", ns, name, err)
		return errors.Trace(err)
	}
	log.Infof("successfully created ServiceAccount %s/%s", ns, name)
	return nil
}
