package cluster

import (
	"context"
	"fmt"
	"strconv"
	"strings"
	"sync"
	"time"

	"github.com/patrickmn/go-cache"
	"github.com/pingcap/dbaas/central/config"
	"github.com/pingcap/dbaas/central/db"
	"github.com/pingcap/dbaas/central/db/entity"
	"github.com/pingcap/dbaas/central/grpc/grpcli"
	"github.com/pingcap/dbaas/central/metrics"
	runtimeutil "github.com/pingcap/dbaas/central/runtime/util"
	"github.com/pingcap/log"
	"github.com/prometheus/client_golang/prometheus"
	"github.com/tidbcloud/idl/pbgen/proto/core"
	log2 "github.com/tidbcloud/pkgv2/log"
	"github.com/tidbcloud/pkgv2/logid"
	"go.uber.org/zap"
	"k8s.io/utils/pointer"
)

var (
	TiDBClusterWaitReadyTime = prometheus.NewDesc(
		prometheus.BuildFQName("dbaas", "tidb_cluster", "wait_ready_duration_seconds"),
		"Duration in seconds to wait for cluster ready",
		[]string{"cluster_id", "name", "tenant", "tenant_name", "project_id", "project_name", metrics.ProviderTypeLbl, "status"},
		nil)

	TiDBClusterInfo = prometheus.NewDesc(
		prometheus.BuildFQName("dbaas", "tidb_cluster", "info"),
		"Information and status of tidb cluster.",
		[]string{"cluster_id", "name", "tenant", "tenant_name", "tenant_plan", "tenant_kind", "project_id", "project_name", "status", "version", "region", "provider", "created_at", "stage", "shoot", "dev_tier"},
		nil)

	TiDBClusterReplicaCount = prometheus.NewGaugeVec(prometheus.GaugeOpts{
		Namespace: "dbaas",
		Subsystem: "tidb_cluster",
		Name:      "replicas",
		Help:      "Number of cluster replicas",
	}, []string{"cluster_id", metrics.ProviderTypeLbl, "component"})
)

// RegisterMetrics registers the metrics which are ONLY used in DbaaS server.
func RegisterMetrics() {
	prometheus.MustRegister(metrics.ReplicationCreationDuration)
	prometheus.MustRegister(metrics.ReplicationPlannedDetachDuration)
	prometheus.MustRegister(metrics.ReplicationForceDetachDuration)
	prometheus.MustRegister(metrics.ChangefeedCreateResultCounter)
	prometheus.MustRegister(metrics.ConnectMysqlErrorCounter)
	prometheus.MustRegister(metrics.WorkerDuration)
	prometheus.MustRegister(metrics.HttpRequestDuration)
	prometheus.MustRegister(metrics.ProxyRequestDuration)
	prometheus.MustRegister(newMetricsCollector(config.Global().GardenerConfig.GardenerProjectName))
	prometheus.MustRegister(newScaleCollector())
	prometheus.MustRegister(metrics.TiDBClusterScaleDuration)
	prometheus.MustRegister(metrics.NewUptimeCollector(time.Now().Unix()))
	prometheus.MustRegister(metrics.CreateClusterDuration)
	prometheus.MustRegister(NewClusterStatusCollector())
	prometheus.MustRegister(TiDBClusterReplicaCount)
}

type MetricsCollector struct {
	// stage is used to find clusters that have running too long in the development or staging environment
	stage        string
	versionCache *cache.Cache
	regionCache  sync.Map
}

func newMetricsCollector(stage string) *MetricsCollector {
	return &MetricsCollector{
		stage:        stage,
		versionCache: cache.New(1*time.Hour, 1*time.Minute),
		regionCache:  sync.Map{},
	}
}

func (c *MetricsCollector) Describe(ch chan<- *prometheus.Desc) {
	ch <- TiDBClusterWaitReadyTime
	ch <- TiDBClusterInfo
}

func (c *MetricsCollector) Collect(ch chan<- prometheus.Metric) {
	var metaInfoTime = time.Now()
	var start = time.Now()
	defer func() {
		log.Info("cluster metrics collect time", zap.Int64("time", time.Since(start).Milliseconds()), zap.Int64("metaInfo-time", metaInfoTime.Sub(start).Milliseconds()))
	}()

	ctx := context.Background()
	ctx = logid.WithLogid(ctx, logid.GenLogid())
	logger := log2.WithContextS(ctx)

	var tenants []*entity.Tenant
	var projects []*entity.Project
	rdb := db.NewProvider().ReadDB(ctx)
	if err := rdb.Model(&entity.Tenant{}).Where("status = ?", entity.TenantStatusActive).Find(&tenants).Error; err != nil {
		logger.Error("cluster prometheus collector query tenant", zap.Error(err))
		return
	}
	if err := rdb.Model(&entity.Project{}).Where("status = ?", entity.ProjectStatusActive).Find(&projects).Error; err != nil {
		logger.Error("cluster prometheus collector query project", zap.Error(err))
		return
	}
	metaInfoTime = time.Now()

	id2tenant := make(map[uint64]*entity.Tenant, len(tenants))
	id2project := make(map[uint64]*entity.Project, len(projects))
	for _, each := range tenants {
		id2tenant[each.ID.Uint64()] = each
	}
	for _, each := range projects {
		id2project[each.ID.Uint64()] = each
	}

	// TODO(chenfei): optimize, query in batches
	resp, err := grpcli.ListClusters(ctx, &core.ListClustersReq{
		Filter: &core.ClusterFilter{
			Deleted: pointer.Bool(false), // only collect non-deleted clusters
		},
	})
	if err != nil {
		logger.Error("cluster prometheus collector query clusters from core-svc", zap.Error(err))
		return
	}
	for _, each := range resp.Clusters {
		c.collectOneCluster(ctx, ch, each, id2tenant, id2project)
	}
}

func (c *MetricsCollector) collectOneCluster(ctx context.Context, ch chan<- prometheus.Metric,
	one *core.Cluster, id2tenant map[uint64]*entity.Tenant, id2project map[uint64]*entity.Project) {
	createdAt := time.Unix(one.CreatedAt, 0)
	dur := time.Since(createdAt).Seconds()
	tenantID := fmt.Sprintf("%d", one.TenantId)
	id := fmt.Sprintf("%d", one.Id)
	projectID := fmt.Sprintf("%d", one.ProjectId)
	shoot := runtimeutil.ShootName(one.ProjectId, entity.Cloud(one.CloudProviderName), one.RegionName)
	tenantName := "unknown"
	tenantKind := entity.TenantKindExternal
	tenantPlan := entity.TenantPlanFreeTrial
	if tenant, ok := id2tenant[one.TenantId]; ok {
		tenantName = tenant.Name
		tenantKind = tenant.Kind
		tenantPlan = tenant.Plan
	}
	projectName := "unknown"
	if project, ok := id2project[one.ProjectId]; ok {
		projectName = project.Name
	}

	if config.CreatingClusterStatusSet[one.Status] {
		// records duration of creating cluster
		ch <- prometheus.MustNewConstMetric(
			TiDBClusterWaitReadyTime,
			prometheus.GaugeValue,
			dur,
			id,
			one.Name,
			tenantID,
			tenantName,
			projectID,
			projectName,
			metrics.GetProviderType(one.IsDevTier),
			strings.ToLower(string(config.ClusterStatusPb2model[one.Status])),
		)
	}

	for _, component := range one.Components {
		name := string(component.ComponentType)
		TiDBClusterReplicaCount.WithLabelValues(id, metrics.GetProviderType(one.IsDevTier), name).Set(float64(component.Replicas))
	}

	ch <- prometheus.MustNewConstMetric(
		TiDBClusterInfo,
		prometheus.GaugeValue,
		1.0,
		id,
		one.Name,
		tenantID,
		tenantName,
		string(tenantPlan),
		string(tenantKind),
		projectID,
		projectName,
		strings.ToLower(string(config.ClusterStatusPb2model[one.Status])),
		one.Version,
		one.RegionName,
		one.CloudProviderName,
		createdAt.String(),
		c.stage,
		shoot,
		strconv.FormatBool(one.IsDevTier),
	)
}
