package resources

import (
	"encoding/json"
	"fmt"
	"sort"
	"strings"

	"github.com/prometheus/client_golang/prometheus"
	corev1 "k8s.io/api/core/v1"
	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
	"k8s.io/apimachinery/pkg/runtime/schema"
	"sigs.k8s.io/yaml"

	gkestatus "github.com/tidbcloud/infra-provider/pkg/gkeheterogeneous/status"
)

var (
	gkeHeterogeneousStatusGVR = schema.GroupVersionResource{
		Group:    "",
		Version:  "v1",
		Resource: "configmaps",
	}

	gkeHeterogeneousPlacementProbeTimestamp = prometheus.NewDesc(
		prometheus.BuildFQName("infra", "exporter", "gke_heterogeneous_placement_probe_timestamp_seconds"),
		"Last attempted GKE heterogeneous placement probe timestamp.",
		[]string{
			"cluster_id",
			"tenant_id",
			"project_id",
			"namespace",
			"probe_status",
		}, nil)
	gkeHeterogeneousSuccessfulPlacementProbeTimestamp = prometheus.NewDesc(
		prometheus.BuildFQName("infra", "exporter", "gke_heterogeneous_successful_placement_probe_timestamp_seconds"),
		"Last successful GKE heterogeneous placement probe timestamp.",
		[]string{
			"cluster_id",
			"tenant_id",
			"project_id",
			"namespace",
			"probe_status",
		}, nil)
	gkeHeterogeneousStatusProbeSuccess = prometheus.NewDesc(
		prometheus.BuildFQName("infra", "exporter", "gke_heterogeneous_status_probe_success"),
		"Whether the last GKE heterogeneous placement snapshot probe succeeded.",
		[]string{
			"cluster_id",
			"tenant_id",
			"project_id",
			"namespace",
			"probe_status",
		}, nil)
	gkeHeterogeneousEnabled = prometheus.NewDesc(
		prometheus.BuildFQName("infra", "exporter", "gke_heterogeneous_enabled"),
		"Whether GKE heterogeneous placement intent is enabled in the latest placement snapshot.",
		[]string{
			"cluster_id",
			"tenant_id",
			"project_id",
			"namespace",
			"probe_status",
		}, nil)
	gkeHeterogeneousValidIntent = prometheus.NewDesc(
		prometheus.BuildFQName("infra", "exporter", "gke_heterogeneous_valid_intent"),
		"Whether GKE heterogeneous placement intent is valid in the latest placement snapshot.",
		[]string{
			"cluster_id",
			"tenant_id",
			"project_id",
			"namespace",
			"probe_status",
		}, nil)
	gkeHeterogeneousClusterFullSynced = prometheus.NewDesc(
		prometheus.BuildFQName("infra", "exporter", "gke_heterogeneous_cluster_full_synced"),
		"Whether the latest GKE heterogeneous placement snapshot was collected after the cluster cache was fully synced.",
		[]string{
			"cluster_id",
			"tenant_id",
			"project_id",
			"namespace",
			"probe_status",
		}, nil)
	gkeHeterogeneousComponentPodsTotal = prometheus.NewDesc(
		prometheus.BuildFQName("infra", "exporter", "gke_heterogeneous_component_pods_total"),
		"Component-level pod counts grouped by placement status in the latest successful GKE heterogeneous placement snapshot.",
		[]string{
			"cluster_id",
			"tenant_id",
			"project_id",
			"namespace",
			"component",
			"tidb_group_id",
			"placement_status",
			"expected_role",
			"expected_instance_type",
			"actual_instance_type",
			"actual_nodepool",
		}, nil)
	gkeHeterogeneousNodesTotal = prometheus.NewDesc(
		prometheus.BuildFQName("infra", "exporter", "gke_heterogeneous_nodes_total"),
		"Unique scheduled base and extra nodes used by this cluster in the latest successful GKE heterogeneous placement snapshot, grouped by nodepool and machine type.",
		[]string{
			"cluster_id",
			"tenant_id",
			"project_id",
			"namespace",
			"nodepool",
			"machine_type",
			"machine_family",
		}, nil)
	gkeHeterogeneousComponentNodesTotal = prometheus.NewDesc(
		prometheus.BuildFQName("infra", "exporter", "gke_heterogeneous_component_nodes_total"),
		"Grouped component node usage from the latest successful GKE heterogeneous placement snapshot. Values are unique only within the full label set and should not be summed across placement_status or expected_role as component unique node totals.",
		[]string{
			"cluster_id",
			"tenant_id",
			"project_id",
			"namespace",
			"component",
			"tidb_group_id",
			"nodepool",
			"machine_type",
			"machine_family",
			"expected_role",
			"placement_status",
		}, nil)
	gkeHeterogeneousComponentPlacementRatio = prometheus.NewDesc(
		prometheus.BuildFQName("infra", "exporter", "gke_heterogeneous_component_placement_ratio"),
		"Ratio of heterogeneous pods over scheduled pods in the latest successful GKE heterogeneous placement snapshot.",
		[]string{
			"cluster_id",
			"tenant_id",
			"project_id",
			"namespace",
			"component",
			"tidb_group_id",
		}, nil)
	gkeHeterogeneousComponentEffectiveRatio = prometheus.NewDesc(
		prometheus.BuildFQName("infra", "exporter", "gke_heterogeneous_component_effective_ratio"),
		"Ratio of heterogeneous pods over desired replicas in the latest successful GKE heterogeneous placement snapshot.",
		[]string{
			"cluster_id",
			"tenant_id",
			"project_id",
			"namespace",
			"component",
			"tidb_group_id",
		}, nil)
	gkeHeterogeneousScaleWorkflowStartTimestamp = prometheus.NewDesc(
		prometheus.BuildFQName("infra", "exporter", "gke_heterogeneous_scale_workflow_start_timestamp_seconds"),
		"Start timestamp for the latest GKE heterogeneous scale workflow matched by active heterogeneous intent.",
		[]string{
			"cluster_id",
			"tenant_id",
			"project_id",
			"namespace",
			"component",
			"tidb_group_id",
			"workflow_namespace",
			"workflow_name",
			"workflow_phase",
		}, nil)
	gkeHeterogeneousScaleWorkflowDuration = prometheus.NewDesc(
		prometheus.BuildFQName("infra", "exporter", "gke_heterogeneous_scale_workflow_duration_seconds"),
		"Duration from Workflow.Status.BeginAt to EndAt for the latest GKE heterogeneous scale workflow matched by active heterogeneous intent.",
		[]string{
			"cluster_id",
			"tenant_id",
			"project_id",
			"namespace",
			"component",
			"tidb_group_id",
			"workflow_namespace",
			"workflow_name",
			"workflow_phase",
		}, nil)
	gkeHeterogeneousNodeProvisionDuration = prometheus.NewDesc(
		prometheus.BuildFQName("infra", "exporter", "gke_heterogeneous_node_provision_duration_seconds"),
		"Approximate heterogeneous node provision duration derived from matched scale workflow duration.",
		[]string{
			"cluster_id",
			"tenant_id",
			"project_id",
			"namespace",
			"component",
			"tidb_group_id",
			"workflow_namespace",
			"workflow_name",
			"workflow_phase",
		}, nil)
	gkeHeterogeneousScaleWorkflowInProgress = prometheus.NewDesc(
		prometheus.BuildFQName("infra", "exporter", "gke_heterogeneous_scale_workflow_in_progress"),
		"Whether the latest GKE heterogeneous scale workflow is currently confirmed in progress.",
		[]string{
			"cluster_id",
			"tenant_id",
			"project_id",
			"namespace",
			"component",
			"tidb_group_id",
			"workflow_namespace",
			"workflow_name",
			"workflow_phase",
		}, nil)
)

type gkeHeterogeneousStatus struct{}

type componentPodMetricGroup struct {
	placementStatus      string
	expectedRole         string
	expectedInstanceType string
	actualInstanceType   string
	actualNodePool       string
	podCount             int32
}

var _ Interface = &gkeHeterogeneousStatus{}

func (s gkeHeterogeneousStatus) HasDedicatedLabel() bool {
	return false
}

func (s gkeHeterogeneousStatus) GVR() schema.GroupVersionResource {
	return gkeHeterogeneousStatusGVR
}

func (s gkeHeterogeneousStatus) Desc() *prometheus.Desc {
	return gkeHeterogeneousPlacementProbeTimestamp
}

func (s gkeHeterogeneousStatus) ExtraDescs() []*prometheus.Desc {
	return []*prometheus.Desc{
		gkeHeterogeneousSuccessfulPlacementProbeTimestamp,
		gkeHeterogeneousStatusProbeSuccess,
		gkeHeterogeneousEnabled,
		gkeHeterogeneousValidIntent,
		gkeHeterogeneousClusterFullSynced,
		gkeHeterogeneousComponentPodsTotal,
		gkeHeterogeneousNodesTotal,
		gkeHeterogeneousComponentNodesTotal,
		gkeHeterogeneousComponentPlacementRatio,
		gkeHeterogeneousComponentEffectiveRatio,
		gkeHeterogeneousScaleWorkflowStartTimestamp,
		gkeHeterogeneousScaleWorkflowDuration,
		gkeHeterogeneousNodeProvisionDuration,
		gkeHeterogeneousScaleWorkflowInProgress,
	}
}

func (s gkeHeterogeneousStatus) FilterOut(u *unstructured.Unstructured) bool {
	cm, err := s.configMap(u)
	if err != nil {
		return true
	}
	return !strings.HasPrefix(cm.Name, gkestatus.ConfigMapNamePrefix)
}

func (s gkeHeterogeneousStatus) Extract(obj *unstructured.Unstructured) prometheus.Metric {
	status, err := s.unmarshalStatus(obj)
	if err != nil {
		return nil
	}
	if status.LastPlacementProbeTime == nil {
		return nil
	}
	return prometheus.MustNewConstMetric(
		gkeHeterogeneousPlacementProbeTimestamp,
		prometheus.GaugeValue,
		float64(status.LastPlacementProbeTime.Unix()),
		status.ClusterID,
		status.TenantID,
		status.ProjectID,
		status.Namespace,
		status.ProbeStatus,
	)
}

func (s gkeHeterogeneousStatus) ExtractMore(obj *unstructured.Unstructured) []prometheus.Metric {
	status, err := s.unmarshalStatus(obj)
	if err != nil {
		return nil
	}
	successfulPlacement := status != nil && status.ProbeStatus == gkestatus.ProbeStatusSuccess

	out := []prometheus.Metric{
		prometheus.MustNewConstMetric(
			gkeHeterogeneousStatusProbeSuccess,
			prometheus.GaugeValue,
			boolFloat(successfulPlacement),
			status.ClusterID,
			status.TenantID,
			status.ProjectID,
			status.Namespace,
			status.ProbeStatus,
		),
		prometheus.MustNewConstMetric(
			gkeHeterogeneousEnabled,
			prometheus.GaugeValue,
			boolFloat(status.Enabled),
			status.ClusterID,
			status.TenantID,
			status.ProjectID,
			status.Namespace,
			status.ProbeStatus,
		),
		prometheus.MustNewConstMetric(
			gkeHeterogeneousValidIntent,
			prometheus.GaugeValue,
			boolFloat(status.ValidIntent),
			status.ClusterID,
			status.TenantID,
			status.ProjectID,
			status.Namespace,
			status.ProbeStatus,
		),
		prometheus.MustNewConstMetric(
			gkeHeterogeneousClusterFullSynced,
			prometheus.GaugeValue,
			boolFloat(status.ClusterFullSynced),
			status.ClusterID,
			status.TenantID,
			status.ProjectID,
			status.Namespace,
			status.ProbeStatus,
		),
	}
	if status.LastSuccessfulPlacementProbeTime != nil {
		out = append(out, prometheus.MustNewConstMetric(
			gkeHeterogeneousSuccessfulPlacementProbeTimestamp,
			prometheus.GaugeValue,
			float64(status.LastSuccessfulPlacementProbeTime.Unix()),
			status.ClusterID,
			status.TenantID,
			status.ProjectID,
			status.Namespace,
			status.ProbeStatus,
		))
	}
	out = append(out, s.extractScaleWorkflow(status)...)
	if !successfulPlacement {
		return out
	}

	for _, component := range status.Components {
		out = append(out, s.extractComponent(status, component)...)
	}
	for _, usage := range status.ClusterNodeUsages {
		out = append(out, prometheus.MustNewConstMetric(
			gkeHeterogeneousNodesTotal,
			prometheus.GaugeValue,
			float64(usage.UsedNodeCount),
			status.ClusterID,
			status.TenantID,
			status.ProjectID,
			status.Namespace,
			usage.NodePool,
			usage.MachineType,
			usage.MachineFamily,
		))
	}
	return out
}

func (s gkeHeterogeneousStatus) extractComponent(status *gkestatus.Snapshot, component gkestatus.ComponentStatus) []prometheus.Metric {
	podGroups := componentPodMetricGroups(component)
	out := make([]prometheus.Metric, 0, len(podGroups)+len(component.NodeUsages)+4)
	for _, group := range podGroups {
		out = append(out, prometheus.MustNewConstMetric(
			gkeHeterogeneousComponentPodsTotal,
			prometheus.GaugeValue,
			float64(group.podCount),
			status.ClusterID,
			status.TenantID,
			status.ProjectID,
			status.Namespace,
			component.ComponentType,
			component.TiDBGroupID,
			gkeHeterogeneousMetricLabel(group.placementStatus),
			gkeHeterogeneousMetricLabel(group.expectedRole),
			gkeHeterogeneousMetricLabel(group.expectedInstanceType),
			gkeHeterogeneousMetricLabel(group.actualInstanceType),
			gkeHeterogeneousMetricLabel(group.actualNodePool),
		))
	}
	for _, usage := range component.NodeUsages {
		out = append(out, prometheus.MustNewConstMetric(
			gkeHeterogeneousComponentNodesTotal,
			prometheus.GaugeValue,
			float64(usage.UsedNodeCount),
			status.ClusterID,
			status.TenantID,
			status.ProjectID,
			status.Namespace,
			component.ComponentType,
			component.TiDBGroupID,
			usage.NodePool,
			usage.MachineType,
			usage.MachineFamily,
			usage.ExpectedRole,
			usage.PlacementStatus,
		))
	}
	if component.PlacementRatio != nil {
		out = append(out, prometheus.MustNewConstMetric(
			gkeHeterogeneousComponentPlacementRatio,
			prometheus.GaugeValue,
			*component.PlacementRatio,
			status.ClusterID,
			status.TenantID,
			status.ProjectID,
			status.Namespace,
			component.ComponentType,
			component.TiDBGroupID,
		))
	}
	if component.EffectiveRatio != nil {
		out = append(out, prometheus.MustNewConstMetric(
			gkeHeterogeneousComponentEffectiveRatio,
			prometheus.GaugeValue,
			*component.EffectiveRatio,
			status.ClusterID,
			status.TenantID,
			status.ProjectID,
			status.Namespace,
			component.ComponentType,
			component.TiDBGroupID,
		))
	}
	return out
}

func componentPodMetricGroups(component gkestatus.ComponentStatus) []componentPodMetricGroup {
	type key struct {
		placementStatus      string
		expectedRole         string
		expectedInstanceType string
		actualInstanceType   string
		actualNodePool       string
	}

	aggs := map[key]int32{}
	for _, group := range component.PodGroups {
		k := key{
			placementStatus:      group.PlacementStatus,
			expectedRole:         group.ExpectedRole,
			expectedInstanceType: group.ExpectedInstanceType,
			actualInstanceType:   group.ActualInstanceType,
			actualNodePool:       group.ActualNodePool,
		}
		aggs[k] += group.PodCount
	}

	out := make([]componentPodMetricGroup, 0, len(aggs))
	for k, count := range aggs {
		out = append(out, componentPodMetricGroup{
			placementStatus:      k.placementStatus,
			expectedRole:         k.expectedRole,
			expectedInstanceType: k.expectedInstanceType,
			actualInstanceType:   k.actualInstanceType,
			actualNodePool:       k.actualNodePool,
			podCount:             count,
		})
	}
	sort.Slice(out, func(i, j int) bool {
		for _, pair := range [][2]string{
			{out[i].placementStatus, out[j].placementStatus},
			{out[i].expectedRole, out[j].expectedRole},
			{out[i].expectedInstanceType, out[j].expectedInstanceType},
			{out[i].actualInstanceType, out[j].actualInstanceType},
			{out[i].actualNodePool, out[j].actualNodePool},
		} {
			if pair[0] < pair[1] {
				return true
			}
			if pair[0] > pair[1] {
				return false
			}
		}
		return false
	})
	return out
}

func (s gkeHeterogeneousStatus) extractScaleWorkflow(status *gkestatus.Snapshot) []prometheus.Metric {
	if status == nil || status.ScaleWorkflow == nil {
		return nil
	}
	workflow := status.ScaleWorkflow
	if workflow.Stale && workflow.DurationSeconds == nil {
		return nil
	}

	labels := []string{
		status.ClusterID,
		status.TenantID,
		status.ProjectID,
		status.Namespace,
		workflow.ComponentType,
		workflow.TiDBGroupID,
		workflow.WorkflowNamespace,
		workflow.WorkflowName,
		workflow.WorkflowPhase,
	}
	out := make([]prometheus.Metric, 0, 3)
	if workflow.BeginAt != nil {
		out = append(out, prometheus.MustNewConstMetric(
			gkeHeterogeneousScaleWorkflowStartTimestamp,
			prometheus.GaugeValue,
			float64(workflow.BeginAt.Unix()),
			labels...,
		))
	}
	if workflow.DurationSeconds != nil {
		out = append(out, prometheus.MustNewConstMetric(
			gkeHeterogeneousScaleWorkflowDuration,
			prometheus.GaugeValue,
			*workflow.DurationSeconds,
			labels...,
		))
		if workflow.Operation == gkestatus.ScaleWorkflowOperationScaleOut ||
			(workflow.Operation == "" && status.Enabled) {
			out = append(out, prometheus.MustNewConstMetric(
				gkeHeterogeneousNodeProvisionDuration,
				prometheus.GaugeValue,
				*workflow.DurationSeconds,
				labels...,
			))
		}
	}
	if workflow.InProgress && !workflow.Stale {
		out = append(out, prometheus.MustNewConstMetric(
			gkeHeterogeneousScaleWorkflowInProgress,
			prometheus.GaugeValue,
			1,
			labels...,
		))
	}
	return out
}

func (s gkeHeterogeneousStatus) unmarshalStatus(obj *unstructured.Unstructured) (*gkestatus.Snapshot, error) {
	cm, err := s.configMap(obj)
	if err != nil {
		return nil, err
	}
	yamlData, ok := cm.Data[gkestatus.ConfigMapDataKey]
	if !ok || yamlData == "" {
		return nil, fmt.Errorf("configmap data key %q not found or empty", gkestatus.ConfigMapDataKey)
	}
	status := &gkestatus.Snapshot{}
	if err := yaml.Unmarshal([]byte(yamlData), status); err != nil {
		return nil, err
	}
	return status, nil
}

func (s gkeHeterogeneousStatus) configMap(obj *unstructured.Unstructured) (*corev1.ConfigMap, error) {
	cm := &corev1.ConfigMap{}
	data, err := obj.MarshalJSON()
	if err != nil {
		return nil, err
	}
	if err := json.Unmarshal(data, cm); err != nil {
		return nil, err
	}
	return cm, nil
}

func boolFloat(v bool) float64 {
	if v {
		return 1
	}
	return 0
}

func gkeHeterogeneousMetricLabel(value string) string {
	value = strings.TrimSpace(value)
	if value == "" {
		return gkestatus.PlacementStatusUnknown
	}
	return value
}
