package resources

import (
	"encoding/json"
	"strconv"
	"strings"

	"github.com/crossplane-contrib/provider-gcp/apis/container/v1beta1"
	commonv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
	"github.com/prometheus/client_golang/prometheus"
	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
	"k8s.io/apimachinery/pkg/runtime/schema"

	"github.com/tidbcloud/eks-provider/infra-exporter/utils"
	"github.com/tidbcloud/infra-provider/pkg/consts"
	"github.com/tidbcloud/infra-provider/pkg/namer"
)

var (
	nodepoolGVR = schema.GroupVersionResource{
		Group:    "container.gcp.crossplane.io",
		Version:  "v1beta1",
		Resource: "nodepools",
	}

	nodepoolInfo = prometheus.NewDesc(
		prometheus.BuildFQName("infra", "exporter", "nodepool"),
		"Information about nodepool CR.",
		[]string{
			"name",
			"network",
			"project",
			"component",
			"cluster",
			"machine_type",
			"locations",
			"delete_timestamp",
			"desired_version",
			"max_size",
			"min_size",
			"status",
			"ready",
			"synced",
		}, nil)
	gkeHeterogeneousNodePoolInfo = prometheus.NewDesc(
		prometheus.BuildFQName("infra", "exporter", "gke_heterogeneous_nodepool_info"),
		"Information about GKE NodePool CRs used by heterogeneous deploy.",
		[]string{
			"name",
			"network",
			"project",
			"component",
			"cluster",
			"machine_type",
			"locations",
			"desired_version",
			"max_size",
			"min_size",
			"status",
			"ready",
			"synced",
			"compute_class_name",
			"desired_compute_class_name",
			"applied_compute_class_name",
			"spec_compute_class_name",
			"compute_class_metadata_consistent",
			"managed_by",
			"api_managed",
		}, nil)
	gkeHeterogeneousNodePoolMaxNodes = prometheus.NewDesc(
		prometheus.BuildFQName("infra", "exporter", "gke_heterogeneous_nodepool_max_nodes"),
		"Configured max node count for GKE heterogeneous NodePool CRs.",
		[]string{
			"name",
			"network",
			"project",
			"component",
			"cluster",
			"machine_type",
			"locations",
			"desired_version",
			"status",
			"ready",
			"synced",
			"compute_class_name",
			"desired_compute_class_name",
			"applied_compute_class_name",
			"spec_compute_class_name",
			"compute_class_metadata_consistent",
			"managed_by",
			"api_managed",
		}, nil)
	gkeHeterogeneousNodePoolComputeClassMetadataReady = prometheus.NewDesc(
		prometheus.BuildFQName("infra", "exporter", "gke_heterogeneous_nodepool_compute_class_metadata_ready"),
		"Whether CR-observable compute-class metadata on a GKE heterogeneous NodePool is complete.",
		[]string{
			"name",
			"network",
			"project",
			"cluster",
			"machine_type",
			"compute_class_name",
			"desired_compute_class_name",
			"applied_compute_class_name",
			"spec_label_compute_class_name",
			"spec_taint_compute_class_name",
			"has_compute_class_metadata",
			"has_isolation_taint",
			"ever_bound",
			"ready",
			"synced",
			"api_managed",
		}, nil)
)

type nodepool struct {
	defaultResource
}

var _ Interface = &nodepool{}

func (n nodepool) HasDedicatedLabel() bool {
	return false
}

func (n nodepool) GVR() schema.GroupVersionResource {
	return nodepoolGVR
}

func (n nodepool) Desc() *prometheus.Desc {
	return nodepoolInfo
}

func (n nodepool) ExtraDescs() []*prometheus.Desc {
	return []*prometheus.Desc{
		gkeHeterogeneousNodePoolInfo,
		gkeHeterogeneousNodePoolMaxNodes,
		gkeHeterogeneousNodePoolComputeClassMetadataReady,
	}
}

func (n nodepool) FilterOut(obj *unstructured.Unstructured) bool {
	// Filter out shared-vpc nodepools
	return utils.IsSharedVPC(obj)
}

func (n nodepool) Extract(obj *unstructured.Unstructured) prometheus.Metric {
	bytes, _ := obj.MarshalJSON()

	var ng v1beta1.NodePool
	_ = json.Unmarshal(bytes, &ng)

	var deleteTS, ready, synced string
	if !ng.DeletionTimestamp.IsZero() {
		deleteTS = ng.DeletionTimestamp.String()
	}

	for _, c := range ng.Status.Conditions {
		if c.Type == commonv1.TypeReady {
			ready = string(c.Status)
		}
		if c.Type == commonv1.TypeSynced {
			synced = string(c.Status)
		}
	}

	var maxSize, minSize int64
	if ng.Spec.ForProvider.Autoscaling != nil {
		maxSize = utils.GetReferencedOrDefault(ng.Spec.ForProvider.Autoscaling.MaxNodeCount, 0)
		minSize = utils.GetReferencedOrDefault(ng.Spec.ForProvider.Autoscaling.MinNodeCount, 0)
	}

	// ticdc nodepools use this label
	component := ng.Labels[namer.ComponentSelectorKey]
	if component == "" {
		// tidb nodepools use this label
		component = ng.Labels["component"]
	}

	clusterID := ng.Labels["cluster"]
	if clusterID == "" {
		clusterID = ng.Labels[namer.ClusterSelectorKey]
	}
	if clusterID == "" {
		// backup nodepools use this label
		clusterID = ng.Labels["tags.tidbcloud.com/cluster"]
	}
	machineType, locations, desiredVersion := gkeNodePoolProviderFields(&ng)

	return prometheus.MustNewConstMetric(
		nodepoolInfo,
		prometheus.GaugeValue,
		1.0,
		ng.Name,
		ng.Labels["tidbcloud-network"],
		getProjectID(&ng),
		component,
		clusterID,
		machineType,
		locations,
		deleteTS,
		desiredVersion,
		strconv.FormatInt(maxSize, 10),
		strconv.FormatInt(minSize, 10),
		ng.Status.AtProvider.Status,
		ready,
		synced,
	)
}

func (n nodepool) ExtractMore(obj *unstructured.Unstructured) []prometheus.Metric {
	bytes, _ := obj.MarshalJSON()

	var ng v1beta1.NodePool
	_ = json.Unmarshal(bytes, &ng)

	if !ng.DeletionTimestamp.IsZero() {
		return nil
	}

	managedBy := ng.Labels[consts.GKEHeterogeneousNodePoolManagedByLabelKey]
	computeClass, desiredComputeClass, appliedComputeClass, specComputeClass, specTaintComputeClass, hasComputeClassMetadata := gkeNodePoolComputeClassNames(&ng)
	if managedBy == "" && computeClass == "" && appliedComputeClass == "" {
		return nil
	}

	var ready, synced string
	for _, c := range ng.Status.Conditions {
		if c.Type == commonv1.TypeReady {
			ready = string(c.Status)
		}
		if c.Type == commonv1.TypeSynced {
			synced = string(c.Status)
		}
	}

	var maxSize, minSize int64
	if ng.Spec.ForProvider.Autoscaling != nil {
		maxSize = utils.GetReferencedOrDefault(ng.Spec.ForProvider.Autoscaling.MaxNodeCount, 0)
		minSize = utils.GetReferencedOrDefault(ng.Spec.ForProvider.Autoscaling.MinNodeCount, 0)
	}

	component := ng.Labels[namer.ComponentSelectorKey]
	if component == "" {
		component = ng.Labels["component"]
	}

	clusterID := ng.Labels["cluster"]
	if clusterID == "" {
		clusterID = ng.Labels[namer.ClusterSelectorKey]
	}
	if clusterID == "" {
		clusterID = ng.Labels["tags.tidbcloud.com/cluster"]
	}

	apiManagedNodePool := managedBy == consts.GKEHeterogeneousNodePoolManagedByLabelValue
	apiManaged := strconv.FormatBool(apiManagedNodePool)
	network := ng.Labels["tidbcloud-network"]
	project := getGKEHeterogeneousProjectID(&ng)
	machineType, locations, desiredVersion := gkeNodePoolProviderFields(&ng)
	hasIsolationTaint := gkeNodePoolHasIsolationTaint(&ng)
	everBound := gkeNodePoolEverBound(&ng)
	metadataReady := gkeNodePoolComputeClassMetadataReady(
		desiredComputeClass,
		appliedComputeClass,
		specComputeClass,
		specTaintComputeClass,
		hasIsolationTaint,
		ready,
		synced,
	)
	metadataConsistent := gkeNodePoolComputeClassMetadataConsistent(desiredComputeClass, appliedComputeClass, specComputeClass)
	if apiManagedNodePool {
		metadataConsistent = metadataReady
	}
	metadataConsistentLabel := strconv.FormatBool(metadataConsistent)
	readinessComputeClass := computeClass
	if readinessComputeClass == "" {
		readinessComputeClass = specTaintComputeClass
	}

	metrics := []prometheus.Metric{
		prometheus.MustNewConstMetric(
			gkeHeterogeneousNodePoolInfo,
			prometheus.GaugeValue,
			1.0,
			ng.Name,
			network,
			project,
			component,
			clusterID,
			machineType,
			locations,
			desiredVersion,
			strconv.FormatInt(maxSize, 10),
			strconv.FormatInt(minSize, 10),
			ng.Status.AtProvider.Status,
			ready,
			synced,
			computeClass,
			desiredComputeClass,
			appliedComputeClass,
			specComputeClass,
			metadataConsistentLabel,
			managedBy,
			apiManaged,
		),
		prometheus.MustNewConstMetric(
			gkeHeterogeneousNodePoolMaxNodes,
			prometheus.GaugeValue,
			float64(maxSize),
			ng.Name,
			network,
			project,
			component,
			clusterID,
			machineType,
			locations,
			desiredVersion,
			ng.Status.AtProvider.Status,
			ready,
			synced,
			computeClass,
			desiredComputeClass,
			appliedComputeClass,
			specComputeClass,
			metadataConsistentLabel,
			managedBy,
			apiManaged,
		),
	}
	if apiManagedNodePool && (hasComputeClassMetadata || everBound) {
		metrics = append(metrics, prometheus.MustNewConstMetric(
			gkeHeterogeneousNodePoolComputeClassMetadataReady,
			prometheus.GaugeValue,
			boolFloat(metadataReady),
			ng.Name,
			network,
			project,
			clusterID,
			machineType,
			readinessComputeClass,
			desiredComputeClass,
			appliedComputeClass,
			specComputeClass,
			specTaintComputeClass,
			strconv.FormatBool(hasComputeClassMetadata),
			strconv.FormatBool(hasIsolationTaint),
			strconv.FormatBool(everBound),
			ready,
			synced,
			apiManaged,
		))
	}
	return metrics
}

func gkeNodePoolProviderFields(np *v1beta1.NodePool) (machineType, locations, desiredVersion string) {
	if np == nil {
		return "", "", ""
	}
	if np.Spec.ForProvider.Config != nil {
		machineType = utils.GetReferencedOrDefault(np.Spec.ForProvider.Config.MachineType, "")
	}
	locations = strings.Join(np.Spec.ForProvider.Locations, ",")
	desiredVersion = utils.GetReferencedOrDefault(np.Spec.ForProvider.Version, "")
	return machineType, locations, desiredVersion
}

func gkeNodePoolComputeClassNames(np *v1beta1.NodePool) (computeClass, desiredComputeClass, appliedComputeClass, specComputeClass, specTaintComputeClass string, hasComputeClassMetadata bool) {
	if np == nil {
		return "", "", "", "", "", false
	}
	if np.Annotations != nil {
		desiredComputeClass = strings.TrimSpace(np.Annotations[consts.AnnoKeyGKENodePoolComputeClass])
		appliedComputeClass = strings.TrimSpace(np.Annotations[consts.AnnoKeyGKENodePoolComputeClassApplied])
	}
	if np.Spec.ForProvider.Config != nil && np.Spec.ForProvider.Config.Labels != nil {
		specComputeClass = strings.TrimSpace(np.Spec.ForProvider.Config.Labels[consts.GKEComputeClassLabelKey])
	}
	specTaintComputeClass = gkeNodePoolComputeClassSpecTaint(np)
	computeClass = desiredComputeClass
	if computeClass == "" {
		computeClass = specComputeClass
	}
	hasComputeClassMetadata = desiredComputeClass != "" ||
		appliedComputeClass != "" ||
		specComputeClass != "" ||
		specTaintComputeClass != ""
	return computeClass, desiredComputeClass, appliedComputeClass, specComputeClass, specTaintComputeClass, hasComputeClassMetadata
}

func gkeNodePoolComputeClassMetadataConsistent(desiredComputeClass, appliedComputeClass, specComputeClass string) bool {
	desiredComputeClass = strings.TrimSpace(desiredComputeClass)
	appliedComputeClass = strings.TrimSpace(appliedComputeClass)
	specComputeClass = strings.TrimSpace(specComputeClass)
	return desiredComputeClass != "" &&
		appliedComputeClass == desiredComputeClass &&
		specComputeClass == desiredComputeClass
}

func gkeNodePoolComputeClassSpecTaint(np *v1beta1.NodePool) string {
	if np == nil || np.Spec.ForProvider.Config == nil {
		return ""
	}
	for _, taint := range np.Spec.ForProvider.Config.Taints {
		if taint == nil {
			continue
		}
		if strings.TrimSpace(taint.Key) != consts.GKEComputeClassLabelKey {
			continue
		}
		if strings.TrimSpace(taint.Effect) != "NO_SCHEDULE" {
			continue
		}
		return strings.TrimSpace(taint.Value)
	}
	return ""
}

func gkeNodePoolHasIsolationTaint(np *v1beta1.NodePool) bool {
	if np == nil || np.Spec.ForProvider.Config == nil {
		return false
	}
	for _, taint := range np.Spec.ForProvider.Config.Taints {
		if taint == nil {
			continue
		}
		if strings.TrimSpace(taint.Key) != consts.GKEHeterogeneousNodePoolIsolationTaintKey {
			continue
		}
		return strings.TrimSpace(taint.Value) == consts.GKEHeterogeneousNodePoolIsolationTaintValue &&
			strings.TrimSpace(taint.Effect) == "NO_SCHEDULE"
	}
	return false
}

func gkeNodePoolEverBound(np *v1beta1.NodePool) bool {
	if np == nil || np.Annotations == nil {
		return false
	}
	everBound, err := strconv.ParseBool(strings.TrimSpace(np.Annotations[consts.AnnoKeyGKEHeterogeneousNodePoolEverBound]))
	return err == nil && everBound
}

func gkeNodePoolComputeClassMetadataReady(desiredComputeClass, appliedComputeClass, specComputeClass, specTaintComputeClass string, hasIsolationTaint bool, ready, synced string) bool {
	desiredComputeClass = strings.TrimSpace(desiredComputeClass)
	appliedComputeClass = strings.TrimSpace(appliedComputeClass)
	specComputeClass = strings.TrimSpace(specComputeClass)
	specTaintComputeClass = strings.TrimSpace(specTaintComputeClass)
	return desiredComputeClass != "" &&
		appliedComputeClass == desiredComputeClass &&
		specComputeClass == desiredComputeClass &&
		specTaintComputeClass == desiredComputeClass &&
		!hasIsolationTaint &&
		ready == "True" &&
		synced == "True"
}

func getGKEHeterogeneousProjectID(np *v1beta1.NodePool) string {
	if np == nil {
		return ""
	}
	if projectID := strings.TrimSpace(np.Labels["tags.tidbcloud.com/project"]); projectID != "" {
		return projectID
	}
	return getProjectID(np)
}

func getProjectID(np *v1beta1.NodePool) string {
	// ticdc/import nodepools use this label
	projectID := np.Labels["project"]

	switch {
	case np.Labels["tidbcloud-namespace"] != "":
		// tidb nodes and admin nodes use this label
		// the value looks like:
		//   - nightly-ms-<projectID>
		//   - staging-<projectID>
		//   - prod-<projectID>
		parts := strings.Split(np.Labels["tidbcloud-namespace"], "-")
		if len(parts) > 1 {
			projectID = parts[len(parts)-1]
		}

	case np.Labels["tags.tidbcloud.com/project"] != "":
		// backup nodepools use this label
		projectID = np.Labels["tags.tidbcloud.com/project"]
	}

	return projectID
}
