package operator

import (
	"fmt"
	"strings"

	"github.com/pingcap/tep/pkg/apis/apiserver"
	"github.com/pingcap/tidb-operator/pkg/apis/pingcap/v1alpha1"
	"github.com/pingcap/tidb-operator/pkg/label"
	"github.com/pingcap/tidb-operator/pkg/util/config"
	corev1 "k8s.io/api/core/v1"
	"k8s.io/apimachinery/pkg/api/resource"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/utils/pointer"
)

const (
	InitSQL = `GRANT ALL PRIVILEGES ON *.* TO 'tidbroot'@'%' WITH GRANT OPTION;
REVOKE SUPER ON *.* FROM 'root'@'%';`

	TiDBSvcPortName             = "mysql-client"
	TiDBSecretName              = "tidb-secret"
	TiDBBackupSecretName        = "tidb-backup-secret"
	TiDBSuperUser               = "tidbroot"
	TiDBDatabase                = "mysql"
	TiDBPort             uint16 = 4000

	S3SecretName   = "s3-object-storage"
	EtcdSecretName = "kube-etcd-client-certs"
)

const (
	NameLabelKey      = "tep.pingcap.com/name"
	TagLabelKeyPrefix = "tep.pingcap.com/tag-"
	CreatedByLabelKey = "tep.pingcap.com/created-by"
	CreatedByLabelVal = "tep"
)

var (
	defaultLogLevel = "info"
	PDDefaultConfig = &v1alpha1.PDConfig{
		Log: &v1alpha1.PDLogConfig{
			Level: "info",
		},
		Replication: &v1alpha1.PDReplicationConfig{
			MaxReplicas:    UInt64Ptr(uint64(3)),
			LocationLabels: v1alpha1.StringSlice{"host"},
		},
	}

	TiKVDefaultConfig = &v1alpha1.TiKVConfig{
		LogLevel: "info",
		ReadPool: &v1alpha1.TiKVReadPoolConfig{
			Coprocessor: &v1alpha1.TiKVCoprocessorReadPoolConfig{
				HighConcurrency:   pointer.Int64Ptr(8),
				NormalConcurrency: pointer.Int64Ptr(8),
				LowConcurrency:    pointer.Int64Ptr(8),
			},
		},
		Storage: &v1alpha1.TiKVStorageConfig{
			BlockCache: &v1alpha1.TiKVBlockCacheConfig{
				Shared:   pointer.BoolPtr(true),
				Capacity: "1GB",
			},
		},
	}
	TiDBDefaultConfig = &v1alpha1.TiDBConfig{
		Log: &v1alpha1.Log{
			Level: &defaultLogLevel,
		}, Performance: &v1alpha1.Performance{
			MaxProcs: UIntPtr(0),
		},
	}
	PodAnnotations  = map[string]string{"fluentbit.io/parser": "pingcap_log"}
	TiDBAnnotations = map[string]string{"fluentbit.io/parser": "pingcap_log", "injector.tumblr.com/request": "auditlog"}
)

type ComponentValues interface {
	SetReplicas(uint32)
	SetNodeSelector(m map[string]string)
	SetAffinity(affinity corev1.Affinity)
	GetResources() corev1.ResourceRequirements
}

type pdSpec struct {
	pd *v1alpha1.PDSpec
}

func (p *pdSpec) SetReplicas(replicas uint32) {
	p.pd.Replicas = int32(replicas)
}
func (p *pdSpec) SetNodeSelector(m map[string]string) {
	p.pd.NodeSelector = m
}
func (p *pdSpec) SetAffinity(affinity corev1.Affinity) {
	p.pd.Affinity = &affinity
}
func (p *pdSpec) GetResources() corev1.ResourceRequirements {
	return p.pd.ResourceRequirements
}

type tikvSpec struct {
	tikv *v1alpha1.TiKVSpec
}

func (t *tikvSpec) SetReplicas(replicas uint32) {
	t.tikv.Replicas = int32(replicas)
}
func (t *tikvSpec) SetNodeSelector(m map[string]string) {
	t.tikv.NodeSelector = m
}
func (t *tikvSpec) SetAffinity(affinity corev1.Affinity) {
	t.tikv.Affinity = &affinity
}
func (t *tikvSpec) GetResources() corev1.ResourceRequirements {
	return t.tikv.ResourceRequirements
}

type tidbSpec struct {
	tidb *v1alpha1.TiDBSpec
}

func (t *tidbSpec) SetReplicas(replicas uint32) {
	t.tidb.Replicas = int32(replicas)
}
func (t *tidbSpec) SetNodeSelector(m map[string]string) {
	t.tidb.NodeSelector = m
}
func (t *tidbSpec) SetAffinity(affinity corev1.Affinity) {
	t.tidb.Affinity = &affinity
}
func (t *tidbSpec) GetResources() corev1.ResourceRequirements {
	return t.tidb.ResourceRequirements
}

func merge(tcv *v1alpha1.TidbCluster, instance *apiserver.TiDBCluster) error {
	tcv.Name = instance.Name
	tcv.Namespace = instance.Name

	if tcv.Labels == nil {
		tcv.Labels = map[string]string{}
	}
	tcv.Labels[CreatedByLabelKey] = CreatedByLabelVal
	if instance.ClusterName != "" {
		tcv.Labels[NameLabelKey] = instance.ClusterName
	}

	if instance.Tags != nil {
		for k := range tcv.Labels {
			if strings.HasPrefix(k, TagLabelKeyPrefix) {
				delete(tcv.Labels, k)
			}
		}

		for _, tag := range instance.Tags {
			tag = strings.TrimSpace(tag)
			if len(tag) == 0 {
				// skip an empty tag, labels are not allowed to be ended with '-' in k8s
				continue
			}
			key := fmt.Sprintf("%s%s", TagLabelKeyPrefix, tag)
			tcv.Labels[key] = ""
		}
	}

	if instance.Version != "" {
		tcv.Spec.PD.BaseImage = getBaseImage(PingCAPRegistryPrefix, label.PDLabelVal)
		tcv.Spec.TiKV.BaseImage = getBaseImage(PingCAPRegistryPrefix, label.TiKVLabelVal)
		tcv.Spec.TiDB.BaseImage = getBaseImage(PingCAPPrivateRegistryPrefix, TiDBEnterpriseImage)
		tcv.Spec.Version = instance.Version
	}

	if instance.EnableTLS != nil {
		tcv.Spec.EnableTLSCluster = instance.EnableTLS
		tcv.Spec.TiDB.EnableTLSClient = instance.EnableTLS
	}

	var serviceExternalTrafficPolicyTypeLocal = corev1.ServiceExternalTrafficPolicyTypeLocal
	tcv.Spec.TiDB.Service.ExternalTrafficPolicy = &serviceExternalTrafficPolicyTypeLocal

	if instance.PD.StorageClassName != "" {
		tcv.Spec.PD.StorageClassName = pointer.StringPtr(instance.PD.StorageClassName)
	}
	if err := setComponent(label.PDLabelVal, &pdSpec{pd: &tcv.Spec.PD}, instance); err != nil {
		return err
	}

	if instance.TiKV.StorageClassName != "" {
		tcv.Spec.TiKV.StorageClassName = pointer.StringPtr(instance.TiKV.StorageClassName)
	}
	if err := setComponent(label.TiKVLabelVal, &tikvSpec{tikv: &tcv.Spec.TiKV}, instance); err != nil {
		return err
	}

	return setComponent(label.TiDBLabelVal, &tidbSpec{tidb: &tcv.Spec.TiDB}, instance)
}

func newDefaultTiDBClusterValues() *v1alpha1.TidbCluster {
	var alwaysPull = corev1.PullAlways
	return &v1alpha1.TidbCluster{
		Spec: v1alpha1.TidbClusterSpec{
			PD: v1alpha1.PDSpec{
				BaseImage: getBaseImage(PingCAPRegistryPrefix, "pd"),
				ComponentSpec: v1alpha1.ComponentSpec{
					Annotations:        PodAnnotations,
					ImagePullPolicy:    &alwaysPull,
					Affinity:           &corev1.Affinity{},
					NodeSelector:       map[string]string{},
					Tolerations:        []corev1.Toleration{},
					HostNetwork:        pointer.BoolPtr(false),
					PodSecurityContext: &corev1.PodSecurityContext{},
				},
				ResourceRequirements: corev1.ResourceRequirements{
					Requests: corev1.ResourceList{
						corev1.ResourceStorage: *resource.NewQuantity(1*1024*1024*1024, resource.BinarySI),
					},
					Limits: corev1.ResourceList{},
				},
				Replicas:         3,
				Service:          nil,
				StorageClassName: pointer.StringPtr("local-storage"),
				Config:           PDDefaultConfig,
			},
			TiDB: v1alpha1.TiDBSpec{
				BaseImage: getBaseImage(PingCAPRegistryPrefix, "tidb"),
				ComponentSpec: v1alpha1.ComponentSpec{
					ImagePullPolicy:    &alwaysPull,
					Affinity:           &corev1.Affinity{},
					NodeSelector:       map[string]string{},
					Tolerations:        []corev1.Toleration{},
					Annotations:        TiDBAnnotations,
					HostNetwork:        pointer.BoolPtr(false),
					PodSecurityContext: &corev1.PodSecurityContext{},
				},
				ResourceRequirements: corev1.ResourceRequirements{
					Limits:   corev1.ResourceList{},
					Requests: corev1.ResourceList{},
				},
				Replicas: 2,
				Service: &v1alpha1.TiDBServiceSpec{
					ServiceSpec: v1alpha1.ServiceSpec{
						Type: corev1.ServiceTypeNodePort,
					},
					ExposeStatus: pointer.BoolPtr(true),
				},
				BinlogEnabled:    pointer.BoolPtr(false),
				MaxFailoverCount: pointer.Int32Ptr(3),
				SeparateSlowLog:  pointer.BoolPtr(true),
				SlowLogTailer: &v1alpha1.TiDBSlowLogTailerSpec{
					ResourceRequirements: corev1.ResourceRequirements{
						Requests: corev1.ResourceList{
							corev1.ResourceCPU:    *resource.NewMilliQuantity(20, resource.DecimalSI),
							corev1.ResourceMemory: *resource.NewQuantity(5*1024*1024, resource.BinarySI),
						},
						Limits: corev1.ResourceList{
							corev1.ResourceCPU:    *resource.NewMilliQuantity(100, resource.DecimalSI),
							corev1.ResourceMemory: *resource.NewQuantity(50*1024*1024, resource.BinarySI),
						},
					},
				},
				EnableTLSClient: pointer.BoolPtr(false),
				Plugins:         []string{"whitelist-1", "audit-1"},
				Config:          TiDBDefaultConfig,
			},
			TiKV: v1alpha1.TiKVSpec{
				BaseImage: getBaseImage(PingCAPRegistryPrefix, "tikv"),
				ComponentSpec: v1alpha1.ComponentSpec{
					ImagePullPolicy:    &alwaysPull,
					Affinity:           &corev1.Affinity{},
					NodeSelector:       map[string]string{},
					Tolerations:        []corev1.Toleration{},
					Annotations:        PodAnnotations,
					HostNetwork:        pointer.BoolPtr(false),
					PodSecurityContext: &corev1.PodSecurityContext{},
				},
				ResourceRequirements: corev1.ResourceRequirements{
					Requests: corev1.ResourceList{
						corev1.ResourceStorage: *resource.NewQuantity(10*1024*1024*1024, resource.BinarySI),
					},
					Limits: corev1.ResourceList{},
				},
				Replicas:         3,
				Privileged:       pointer.BoolPtr(false),
				StorageClassName: pointer.StringPtr("local-storage"),
				MaxFailoverCount: pointer.Int32Ptr(3),
				Config:           TiKVDefaultConfig,
			},
			Pump: &v1alpha1.PumpSpec{
				ComponentSpec:        v1alpha1.ComponentSpec{},
				ResourceRequirements: corev1.ResourceRequirements{},
				StorageClassName:     pointer.StringPtr("local-storage"),
				Replicas:             0,
				GenericConfig:        config.GenericConfig{},
				SetTimeZone:          nil,
			},
			Helper: &v1alpha1.HelperSpec{
				Image: pointer.StringPtr(getImage("", BusyboxImage, BusyboxVersion)),
			},
			Services: []v1alpha1.Service{
				{Name: "pd", Type: "ClusterIP"},
			},
			SchedulerName:        "tidb-scheduler",
			PVReclaimPolicy:      "Retain",
			ConfigUpdateStrategy: v1alpha1.ConfigUpdateStrategyRollingUpdate,
			EnableTLSCluster:     pointer.BoolPtr(false),
			Timezone:             "UTC",
			HostNetwork:          pointer.BoolPtr(false),
		},
	}
}

func newDefaultMonitor() *v1alpha1.TidbMonitor {
	var pullIfNotExits = corev1.PullIfNotPresent
	return &v1alpha1.TidbMonitor{
		Spec: v1alpha1.TidbMonitorSpec{
			Clusters: []v1alpha1.TidbClusterRef{},
			Prometheus: v1alpha1.PrometheusSpec{
				MonitorContainer: v1alpha1.MonitorContainer{
					BaseImage:       getBaseImage(PrometheusRegistryPrefix, PrometheusImage),
					Version:         PrometheusVersion,
					ImagePullPolicy: &pullIfNotExits,
				},
				LogLevel: "info",
				Service: v1alpha1.ServiceSpec{
					Type: corev1.ServiceTypeNodePort,
				},
				ReserveDays: 12,
			},
			Grafana: &v1alpha1.GrafanaSpec{
				MonitorContainer: v1alpha1.MonitorContainer{
					Resources:       corev1.ResourceRequirements{},
					BaseImage:       getBaseImage(GrafanaRegistryPrefix, GrafanaImage),
					Version:         GrafanaVersion,
					ImagePullPolicy: &pullIfNotExits,
				},
				LogLevel: "info",
				Service: v1alpha1.ServiceSpec{
					Type: corev1.ServiceTypeNodePort,
				},
				Username: "admin",
				Password: "admin",
				Envs: map[string]string{
					"GF_AUTH_ANONYMOUS_ENABLED":  "true",
					"GF_AUTH_ANONYMOUS_ORG_NAME": "Main Org.",
					"GF_AUTH_ANONYMOUS_ORG_ROLE": "Viewer",
				},
			},
			Reloader: v1alpha1.ReloaderSpec{
				MonitorContainer: v1alpha1.MonitorContainer{
					Resources:       corev1.ResourceRequirements{},
					BaseImage:       getBaseImage(PingCAPRegistryPrefix, TiDBMonitorReloaderImage),
					Version:         TiDBMonitorReloaderVersion,
					ImagePullPolicy: &pullIfNotExits,
				},
				Service: v1alpha1.ServiceSpec{
					Type: corev1.ServiceTypeNodePort,
				},
			},
			Initializer: v1alpha1.InitializerSpec{
				MonitorContainer: v1alpha1.MonitorContainer{
					Resources:       corev1.ResourceRequirements{},
					BaseImage:       getBaseImage(PingCAPRegistryPrefix, "tidb-monitor-initializer"),
					ImagePullPolicy: &pullIfNotExits,
				},
				Envs: map[string]string{},
			},
			ImagePullPolicy:  pullIfNotExits,
			Persistent:       true,
			StorageClassName: pointer.StringPtr("local-storage"),
			Storage:          "10Gi",
			//TODO: we need to get KubePrometheusURL from apiserver's startup
			KubePrometheusURL: pointer.StringPtr("http://k8s-prometheus.monitoring.svc:9090"),
			NodeSelector:      map[string]string{},
			Annotations:       map[string]string{},
			Tolerations:       []corev1.Toleration{},
		},
	}
}

func mergeMonitor(m *v1alpha1.TidbMonitor, instance *apiserver.TiDBCluster) {
	m.Name = instance.Name
	m.Spec.Initializer.BaseImage = getBaseImage(PingCAPRegistryPrefix, TiDBMonitorInitializerImage)
	m.Spec.Initializer.Version = instance.Version
	m.Spec.Clusters = []v1alpha1.TidbClusterRef{{Name: instance.Name, Namespace: instance.Name}}
	if instance.Monitor.StorageClassName != "" {
		m.Spec.StorageClassName = &instance.Monitor.StorageClassName
	}
	if instance.Monitor.AlertmanagerURL != "" {
		m.Spec.AlertmanagerURL = &instance.Monitor.AlertmanagerURL
	}
}

func newInitializer(instance *apiserver.TiDBCluster) *v1alpha1.TidbInitializer {
	secretName := TiDBSecretName
	return &v1alpha1.TidbInitializer{
		ObjectMeta: metav1.ObjectMeta{
			Name: instance.Name,
		},
		Spec: v1alpha1.TidbInitializerSpec{
			Image:          getImage(MySQLClientRegistryPrefix, MySQLClientImage, MySQLClientVersion),
			InitSql:        pointer.StringPtr(InitSQL),
			PasswordSecret: &secretName,
			Clusters:       v1alpha1.TidbClusterRef{Namespace: instance.Name, Name: instance.Name},
		},
	}
}
