package resources

import (
	"fmt"
	"sync"
	"time"

	"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"
)

type Interface interface {
	GVR() schema.GroupVersionResource
	Desc() *prometheus.Desc
	FilterOut(*unstructured.Unstructured) bool
	Extract(*unstructured.Unstructured) prometheus.Metric
	ExtractMore(_ *unstructured.Unstructured) []prometheus.Metric

	// HasDedicatedLabel indicates if this kind of custom resource has the label `tags.tidbcloud.com/servicetype=dedicated`.
	HasDedicatedLabel() bool
}

type defaultResource struct {
}

func (r defaultResource) ExtractMore(_ *unstructured.Unstructured) []prometheus.Metric {
	return nil
}

func (r defaultResource) ExtraDescs() []*prometheus.Desc {
	return nil
}

type Collector struct {
	Res     Interface
	metrics sync.Map
}

var _ prometheus.Collector = &Collector{}

func newCollector(res Interface) *Collector {
	return &Collector{
		Res:     res,
		metrics: sync.Map{},
	}
}

func (c *Collector) Describe(ch chan<- *prometheus.Desc) {
	ch <- c.Res.Desc()
	if extraDescriber, ok := c.Res.(interface{ ExtraDescs() []*prometheus.Desc }); ok {
		for _, desc := range extraDescriber.ExtraDescs() {
			if desc != nil {
				ch <- desc
			}
		}
	}
}

func (c *Collector) Collect(ch chan<- prometheus.Metric) {
	beg := time.Now()
	cnt := 0
	utils.Logger.Info("start to collect metrics", "resource", c.Res.GVR())
	c.metrics.Range(func(key, value interface{}) bool {
		for _, metrics := range value.([]prometheus.Metric) {
			ch <- metrics
		}
		cnt++
		return true
	})
	utils.Logger.Info("finish collecting metrics", "resource", c.Res.GVR(), "duration", time.Since(beg), "count", cnt)
}

func (c *Collector) store(obj *unstructured.Unstructured) {
	var metricList []prometheus.Metric
	metricsInfo := c.Res.Extract(obj)
	if metricsInfo != nil {
		metricList = append(metricList, metricsInfo)
	}
	for _, each := range c.Res.ExtractMore(obj) {
		if each != nil {
			metricList = append(metricList, each)
		}
	}
	c.metrics.Store(string(obj.GetUID()), metricList)
}

func (c *Collector) OnAdd(obj interface{}) {
	unstructuredObj := obj.(*unstructured.Unstructured)

	if c.Res.FilterOut(unstructuredObj) {
		return
	}

	c.store(unstructuredObj)
}

func (c *Collector) OnUpdate(oldObj, newObj interface{}) {
	oldObjUnstructured := oldObj.(*unstructured.Unstructured)
	newObjUnstructured := newObj.(*unstructured.Unstructured)

	if oldObjUnstructured.GetResourceVersion() == newObjUnstructured.GetResourceVersion() || c.Res.FilterOut(newObjUnstructured) {
		return
	}

	c.store(newObjUnstructured)
}

func (c *Collector) OnDelete(obj interface{}) {
	unstructuredObj, ok := obj.(*unstructured.Unstructured)
	if !ok {
		utils.Logger.Error(fmt.Errorf("failed to convert obj to unstructured"), "failed to convert obj to unstructured", "obj", obj)
		return
	}

	if c.Res.FilterOut(unstructuredObj) {
		return
	}

	c.metrics.Delete(string(unstructuredObj.GetUID()))
}

func GetAllCollectors(provider string) []*Collector {
	collectors := []*Collector{
		newCollector(network{}),
		newCollector(cluster{}),
		newCollector(upgradeOperation{}),
		newCollector(configChange{}),
		newCollector(workflow{}),
		newCollector(scaleStatus{}),
	}

	switch provider {
	case "eks":
		collectors = append(collectors,
			newCollector(vpcendpoint{}),
			newCollector(nodegroup{}),
			newCollector(eks{}),
			newCollector(autoscalinggroup{}))
	case "gke":
		collectors = append(collectors,
			newCollector(gke{}),
			newCollector(nodepool{}),
			newCollector(gkeHeterogeneousStatus{}))
	case "aks":
		collectors = append(collectors,
			newCollector(aks{}),
		)
	case "":
		collectors = append(collectors,
			newCollector(vpcendpoint{}),
			newCollector(nodegroup{}),
			newCollector(eks{}),
			newCollector(autoscalinggroup{}),
			newCollector(gke{}),
			newCollector(nodepool{}),
			newCollector(gkeHeterogeneousStatus{}))
	}

	return collectors
}
