package controller

import (
	"fmt"
	"strings"

	"github.com/jinzhu/gorm"
	"github.com/pingcap/tep/db/entity"
	"github.com/pingcap/tep/pkg/apis/apiserver"
	"github.com/pingcap/tep/pkg/operator"
	"github.com/pingcap/tidb-operator/pkg/apis/pingcap/v1alpha1"
	"github.com/pingcap/tidb-operator/pkg/label"
	corev1 "k8s.io/api/core/v1"
	"k8s.io/apimachinery/pkg/api/resource"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

func (c *Controller) GetTidbCluster(namespace, name string, opts metav1.GetOptions) (*v1alpha1.TidbCluster, error) {
	return c.clusterCli.PingcapV1alpha1().TidbClusters(namespace).Get(name, opts)
}

func (c *Controller) GetTidbClusters(namespace string, opts metav1.ListOptions) (*v1alpha1.TidbClusterList, error) {
	return c.clusterCli.PingcapV1alpha1().TidbClusters(namespace).List(opts)
}

func getAzs(affinity *corev1.Affinity) []string {
	azs := []string{}

	if affinity != nil && affinity.NodeAffinity != nil {
		nodeSelector := affinity.NodeAffinity.RequiredDuringSchedulingIgnoredDuringExecution

		for _, nodeSelectorTerms := range nodeSelector.NodeSelectorTerms {
			for _, nodeSelectorRequirement := range nodeSelectorTerms.MatchExpressions {
				if nodeSelectorRequirement.Key == "zone" && nodeSelectorRequirement.Operator == corev1.NodeSelectorOpIn {
					return nodeSelectorRequirement.Values
				}
			}
		}
	}

	return azs
}

func GetPDAzs(tc *v1alpha1.TidbCluster) []string {
	return getAzs(tc.Spec.PD.Affinity)
}

func GetTiDBAzs(tc *v1alpha1.TidbCluster) []string {
	return getAzs(tc.Spec.TiDB.Affinity)
}

func GetTiKVAzs(tc *v1alpha1.TidbCluster) []string {
	return getAzs(tc.Spec.TiKV.Affinity)
}

func (c *Controller) GetInstanceAzs(tc *v1alpha1.TidbCluster) []string {
	var azs []string
	pdAzs := GetPDAzs(tc)
	tidbAzs := GetTiDBAzs(tc)
	tikvAzs := GetTiKVAzs(tc)

	var list []string
	list = append(list, pdAzs...)
	list = append(list, tidbAzs...)
	list = append(list, tikvAzs...)

	keys := make(map[string]bool)
	for _, entry := range list {
		if _, ok := keys[entry]; !ok {
			keys[entry] = true
			azs = append(azs, entry)
		}
	}

	return azs
}

func (c *Controller) GetNameAndTags(tc *v1alpha1.TidbCluster) (name string, tags []string) {
	labels := tc.GetLabels()
	tags = make([]string, 0)
	for k, v := range labels {
		if strings.HasPrefix(k, operator.TagLabelKeyPrefix) {
			tags = append(tags, strings.TrimPrefix(k, operator.TagLabelKeyPrefix))
		}
		if k == operator.NameLabelKey {
			name = v
		}
	}
	return
}

func (c *Controller) GetPodList(namespace, component string) (*corev1.PodList, error) {
	var labelSelector string
	switch component {
	case label.PDLabelVal:
		labelSelector = label.New().
			Instance(namespace).PD().String()
	case label.TiKVLabelVal:
		labelSelector = label.New().
			Instance(namespace).TiKV().String()
	case label.TiDBLabelVal:
		labelSelector = label.New().
			Instance(namespace).TiDB().String()
	default:
		return nil, fmt.Errorf("unknown component type[%s]", component)
	}

	listOptions := metav1.ListOptions{LabelSelector: labelSelector}
	podList, err := c.kubeCli.CoreV1().Pods(namespace).List(listOptions)
	return podList, err
}

func (c *Controller) GetPVList(namespace, instanceID, component string) (*corev1.PersistentVolumeList, error) {
	var labelSelector string
	switch component {
	case label.PDLabelVal:
		labelSelector = label.New().
			Instance(instanceID).Namespace(namespace).PD().String()
	case label.TiKVLabelVal:
		labelSelector = label.New().
			Instance(instanceID).Namespace(namespace).TiKV().String()
	case label.TiDBLabelVal:
		labelSelector = label.New().
			Instance(instanceID).Namespace(namespace).TiDB().String()
	default:
		return nil, fmt.Errorf("unknown component type[%s]", component)
	}

	listOptions := metav1.ListOptions{LabelSelector: labelSelector}
	pvList, err := c.kubeCli.CoreV1().PersistentVolumes().List(listOptions)
	return pvList, err
}

func (c *Controller) GetComponentResourceTemplate(tc *v1alpha1.TidbCluster, component string) (*apiserver.ResourceTemplate, error) {
	var (
		limit       corev1.ResourceList
		cpu, memory *resource.Quantity
	)

	switch component {
	case label.PDLabelVal:
		limit = tc.Spec.PD.Limits
	case label.TiKVLabelVal:
		limit = tc.Spec.TiKV.Limits
	case label.TiDBLabelVal:
		limit = tc.Spec.TiDB.Limits
	default:
		return nil, fmt.Errorf("unknown component[%s]", component)
	}

	if limit == nil {
		return nil, fmt.Errorf("component[%s]'s resource limit not set", component)
	}

	cpu = limit.Cpu()
	memory = limit.Memory()

	resourceTemplate := &apiserver.ResourceTemplate{
		CPU:    uint32(cpu.Value()),
		Memory: uint32(memory.Value() / (1024 * 1024 * 1024)),
	}
	return resourceTemplate, nil
}

func (c *Controller) GetComponentImage(component string, pod corev1.Pod) string {
	switch component {
	case label.PDLabelVal, label.TiKVLabelVal, label.TiDBLabelVal:
		for _, containerStatus := range pod.Status.ContainerStatuses {
			if containerStatus.Name == component {
				return containerStatus.Image
			}
		}
	}
	return ""
}

func (c *Controller) GetComponentInfo(tc *v1alpha1.TidbCluster, component string, pod corev1.Pod, db *gorm.DB) (apiserver.ComponentInfo, error) {
	componentInfo := apiserver.ComponentInfo{}

	resourceTemplate, err := c.GetComponentResourceTemplate(tc, component)
	if err != nil {
		msg := fmt.Errorf("failed to get ResourceTemplate of component[%s], err: %v", component, err)
		return componentInfo, msg
	}
	componentInfo.ResourceTemplate = resourceTemplate

	podImage := c.GetComponentImage(component, pod)
	if podImage != "" {
		componentInfo.Version = c.GetImageVersion(podImage)
	}

	componentInfo.PodPhase = strings.ToLower(string(pod.Status.Phase))
	componentInfo.NodeName = pod.Spec.NodeName

	machine := entity.Machine{}
	if err := db.First(&machine, "id = ?", componentInfo.NodeName).Error; err != nil {
		if gorm.IsRecordNotFoundError(err) {
			msg := fmt.Errorf("machine with name[%s] not found in the machine table", componentInfo.NodeName)
			return componentInfo, msg
		}
		msg := fmt.Errorf("failed to query machine table with error[%v]", err)
		return componentInfo, msg
	}

	az := &entity.Az{}
	if az, err = c.GetAzByID(machine.AzID, db); err != nil {
		msg := fmt.Errorf("failed to get az by id[%d], err: %s", machine.AzID, err.Error())
		return componentInfo, msg
	}
	componentInfo.AZ = az.Name

	return componentInfo, nil
}
