package resources

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

	"github.com/prometheus/client_golang/prometheus"
	"github.com/samber/lo"
	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
	"k8s.io/apimachinery/pkg/runtime/schema"

	cloudv1 "github.com/tidbcloud/infra-api/api/v1"
	"github.com/tidbcloud/infra-provider/pkg/consts"
	"github.com/tidbcloud/infra-provider/pkg/namer"
)

var (
	clusterGVR = schema.GroupVersionResource{
		Group:    "cloud.pingcap.com",
		Version:  "v1",
		Resource: "clusters",
	}

	clusterInfo = prometheus.NewDesc(
		prometheus.BuildFQName("infra", "exporter", "cluster"),
		"Information about Cluster CR.",
		[]string{
			"name",
			"namespace",
			"generation",
			"tenant_id",
			"project_id",
			"network_name",
			"dataplane",
			"pd_replicas",
			"tidb_replicas",
			"tikv_replicas",
			"tiflash_replicas",
			"tiproxy_replicas",
			"version",
			"reason",
			"status",
			"pd_status",
			"tidb_status",
			"tikv_status",
			"tiflash_status",
			"tiproxy_status",
			"paused",
			"privatelinkserivce_status",
		}, nil)

	clusterBug = prometheus.NewDesc(
		prometheus.BuildFQName("infra", "exporter", "cluster_bug"),
		"Information about Cluster CR.",
		[]string{
			"name",
			"namespace",
			"generation",
			"tenant_id",
			"project_id",
			"network_name",
			"dataplane",
			"version",
			"status",
			"paused",
			"bug_name",
		}, nil)
	clusterFeature = prometheus.NewDesc(
		prometheus.BuildFQName("infra", "exporter", "cluster_feature"),
		"Enabled features in Cluster CR.",
		[]string{
			"name",
			"namespace",
			"generation",
			"tenant_id",
			"project_id",
			"network_name",
			"dataplane",
			"version",
			"status",
			"paused",
			"feature_name",
			"anno_value",
		}, nil)
	gkeHeterogeneousDeployInfo = prometheus.NewDesc(
		prometheus.BuildFQName("infra", "exporter", "gke_heterogeneous_deploy_info"),
		"GKE heterogeneous deploy binding from Cluster CR.",
		[]string{
			"name",
			"namespace",
			"generation",
			"tenant_id",
			"project_id",
			"network_name",
			"dataplane",
			"version",
			"status",
			"paused",
			"component",
			"tidb_group_id",
			"annotation_key",
			"compute_class_name",
			"base_instance_type",
			"role",
			"instance_type",
			"nodepool",
			"valid",
		}, nil)
)

type cluster struct{}

var _ Interface = &cluster{}

func (c cluster) HasDedicatedLabel() bool {
	return true
}

func (c cluster) GVR() schema.GroupVersionResource {
	return clusterGVR
}

func (c cluster) Desc() *prometheus.Desc {
	return clusterInfo
}

func (c cluster) ExtraDescs() []*prometheus.Desc {
	return []*prometheus.Desc{
		clusterBug,
		clusterFeature,
		gkeHeterogeneousDeployInfo,
	}
}

func (c cluster) FilterOut(u *unstructured.Unstructured) bool {
	return false
}

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

	var clusterCR cloudv1.Cluster
	_ = json.Unmarshal(bytes, &clusterCR)

	var (
		version       string
		pdStatus      cloudv1.PDStatus
		tidbStatus    cloudv1.TiDBStatus
		tikvStatus    cloudv1.TiKVStatus
		tiflashStatus cloudv1.TiFlashStatus
		tiproxyStatus cloudv1.TiProxyStatus
	)

	paused := false
	if clusterCR.Spec.Paused != nil {
		paused = *clusterCR.Spec.Paused
	}

	if clusterCR.Status.DB != nil {
		version = clusterCR.Status.DB.Version
		if clusterCR.Status.DB.PD != nil {
			pdStatus = *clusterCR.Status.DB.PD
		}
		if clusterCR.Status.DB.TiDB != nil {
			tidbStatus = *clusterCR.Status.DB.TiDB
		}
		if clusterCR.Status.DB.TiKV != nil {
			tikvStatus = *clusterCR.Status.DB.TiKV
		}
		if clusterCR.Status.DB.TiFlash != nil {
			tiflashStatus = *clusterCR.Status.DB.TiFlash
		}
		if clusterCR.Status.DB.TiProxy != nil {
			tiproxyStatus = *clusterCR.Status.DB.TiProxy
		}
	}
	privateLinkServiceStatus := "NotConfigure"
	if clusterCR.Status.SqlEndpoints.PrivateLinkService != nil {
		privateLinkServiceStatus = string(clusterCR.Status.SqlEndpoints.PrivateLinkService.Phase)
	}

	return prometheus.MustNewConstMetric(
		clusterInfo,
		prometheus.GaugeValue,
		1.0,
		clusterCR.Name,
		clusterCR.Namespace,
		strconv.FormatInt(clusterCR.Generation, 10),
		clusterCR.Labels[consts.TenantLabelKey],
		clusterCR.Labels[consts.ProjectLabelKey],
		clusterCR.Spec.NetworkName,
		clusterCR.Spec.Kubernetes.Name,
		strconv.Itoa(clusterCR.Spec.Cluster.PD.Replicas),
		strconv.Itoa(clusterCR.Spec.Cluster.TiDB.Replicas),
		strconv.Itoa(clusterCR.Spec.Cluster.TiKV.Replicas),
		strconv.Itoa(clusterCR.Spec.Cluster.TiFlash.Replicas),
		strconv.Itoa(clusterCR.Spec.Cluster.TiProxy.Replicas),
		version,
		clusterCR.Status.Reason,
		string(clusterCR.Status.Phase),
		string(pdStatus.Status),
		string(tidbStatus.Status),
		string(tikvStatus.Status),
		string(tiflashStatus.Status),
		string(tiproxyStatus.Status),
		strconv.FormatBool(paused),
		privateLinkServiceStatus,
	)
}

func (c cluster) ExtractMore(obj *unstructured.Unstructured) []prometheus.Metric {
	bytes, _ := obj.MarshalJSON()
	var clusterCR cloudv1.Cluster
	_ = json.Unmarshal(bytes, &clusterCR)

	var (
		version string
		paused  = lo.FromPtr(clusterCR.Spec.Paused)
	)
	if clusterCR.Status.DB != nil {
		version = clusterCR.Status.DB.Version
	}

	out := c.extractBugList(clusterCR, version, paused)
	out = append(out, c.extractFeatureList(clusterCR, version, paused)...)
	out = append(out, c.extractGKEHeterogeneousDeployList(clusterCR, version, paused)...)

	return out
}

func (c cluster) extractBugList(clusterCR cloudv1.Cluster, version string, paused bool) []prometheus.Metric {
	var out []prometheus.Metric
	for _, bugName := range clusterCR.BugList() {
		if bugName == "" {
			continue
		}

		out = append(out, prometheus.MustNewConstMetric(
			clusterBug,
			prometheus.GaugeValue,
			1.0,
			clusterCR.Name,
			clusterCR.Namespace,
			strconv.FormatInt(clusterCR.Generation, 10),
			clusterCR.Labels[consts.TenantLabelKey],
			clusterCR.Labels[consts.ProjectLabelKey],
			clusterCR.Spec.NetworkName,
			clusterCR.Spec.Kubernetes.Name,
			version,
			string(clusterCR.Status.Phase),
			strconv.FormatBool(paused),
			bugName,
		))
	}
	return out
}

func (c cluster) extractFeatureList(clusterCR cloudv1.Cluster, version string, paused bool) []prometheus.Metric {
	var out []prometheus.Metric
	for k, v := range clusterCR.Annotations {
		if !strings.HasPrefix(k, cloudv1.AnnoKeyFeatureNamePrefix) {
			continue
		}

		featName := strings.TrimPrefix(k, cloudv1.AnnoKeyFeatureNamePrefix)
		value := 0.0
		if v == consts.AnnoValTrue {
			value = 1.0
		}

		out = append(out, prometheus.MustNewConstMetric(
			clusterFeature,
			prometheus.GaugeValue,
			value,
			clusterCR.Name,
			clusterCR.Namespace,
			strconv.FormatInt(clusterCR.Generation, 10),
			clusterCR.Labels[consts.TenantLabelKey],
			clusterCR.Labels[consts.ProjectLabelKey],
			clusterCR.Spec.NetworkName,
			clusterCR.Spec.Kubernetes.Name,
			version,
			string(clusterCR.Status.Phase),
			strconv.FormatBool(paused),
			featName,
			v,
		))
	}
	return out
}

func (c cluster) extractGKEHeterogeneousDeployList(clusterCR cloudv1.Cluster, version string, paused bool) []prometheus.Metric {
	if clusterCR.GetProviderType() != cloudv1.ProviderTypeGKE {
		return nil
	}

	var out []prometheus.Metric
	appendBindingMetrics := func(component cloudv1.ComponentType, tidbGroupID, annotationKey, baseInstanceType string) {
		raw := strings.TrimSpace(clusterCR.GetAnnotations()[annotationKey])
		if raw == "" {
			return
		}
		rawComputeClassName := ""
		decoded, decodeErr := cloudv1.DecodeGKEComputeClassBindingAnnotation(raw)
		if decodeErr == nil {
			// Keep only the top-level raw ComputeClass name on invalid-but-decodable
			// annotations. Per-binding labels are emitted only after full validation
			// to avoid exposing stale or untrusted high-cardinality details.
			rawComputeClassName = strings.TrimSpace(decoded.ComputeClassName)
		}
		annotation, explicit, err := cloudv1.ClusterGKEComputeClassBindingAnnotation(
			&clusterCR,
			annotationKey,
			baseInstanceType,
			namer.GKENodePoolNameForCluster(&clusterCR, baseInstanceType),
		)
		if err != nil || !explicit {
			out = append(out, c.gkeHeterogeneousDeployMetric(clusterCR, version, paused, component, tidbGroupID, annotationKey, rawComputeClassName, baseInstanceType, "", "", "", false))
			return
		}
		for _, binding := range annotation.NodePools {
			out = append(out, c.gkeHeterogeneousDeployMetric(
				clusterCR,
				version,
				paused,
				component,
				tidbGroupID,
				annotationKey,
				annotation.ComputeClassName,
				baseInstanceType,
				binding.Role,
				binding.InstanceType,
				binding.NodePoolName,
				true,
			))
		}
	}

	appendBindingMetrics(
		cloudv1.ComponentTiKV,
		"",
		cloudv1.AnnoKeyGKEComputeClassForTiKVBindings,
		strings.TrimSpace(clusterCR.Spec.Cluster.TiKV.InstanceType),
	)

	for _, ref := range activeTiDBComputeClassAnnotationRefs(&clusterCR) {
		appendBindingMetrics(
			cloudv1.ComponentTiDB,
			ref.TiDBGroupID,
			ref.AnnotationKey,
			clusterCR.TiDBInstanceTypeForGroup(ref.TiDBGroupID),
		)
	}

	return out
}

func activeTiDBComputeClassAnnotationRefs(clusterCR *cloudv1.Cluster) []cloudv1.TiDBComputeClassAnnotationRef {
	if clusterCR == nil {
		return nil
	}
	refs := clusterCR.ConfiguredTiDBComputeClassAnnotationRefs()
	out := make([]cloudv1.TiDBComputeClassAnnotationRef, 0, len(refs))
	for _, ref := range refs {
		component, err := clusterCR.GetComponentSpecInGroup(strings.TrimSpace(ref.TiDBGroupID), cloudv1.ComponentTiDB)
		if err == nil && component != nil {
			out = append(out, ref)
		}
	}
	return out
}

func (c cluster) gkeHeterogeneousDeployMetric(
	clusterCR cloudv1.Cluster,
	version string,
	paused bool,
	component cloudv1.ComponentType,
	tidbGroupID,
	annotationKey,
	computeClassName,
	baseInstanceType,
	role,
	instanceType,
	nodePoolName string,
	valid bool,
) prometheus.Metric {
	return prometheus.MustNewConstMetric(
		gkeHeterogeneousDeployInfo,
		prometheus.GaugeValue,
		1.0,
		clusterCR.Name,
		clusterCR.Namespace,
		strconv.FormatInt(clusterCR.Generation, 10),
		clusterCR.GetTenantID(),
		clusterCR.GetProjectID(),
		clusterCR.GetNetworkRef().Name,
		clusterCR.Spec.Kubernetes.Name,
		version,
		string(clusterCR.Status.Phase),
		strconv.FormatBool(paused),
		string(component),
		tidbGroupID,
		annotationKey,
		computeClassName,
		baseInstanceType,
		role,
		instanceType,
		nodePoolName,
		strconv.FormatBool(valid),
	)
}
