package metadb

import (
	"context"
	"fmt"
	"strconv"
	"time"

	"github.com/pingcap/dbaas/central/db/entity"
	clustermodel "github.com/pingcap/dbaas/central/model/cluster"
	"github.com/pingcap/dbaas/central/model/integration"
	tidbcomp "github.com/pingcap/dbaas/pkg/tidb/component"
	"github.com/tidbcloud/metrics/internal/datasource"
	"github.com/tidbcloud/metrics/internal/datasource/central_thanos"
	"github.com/tidbcloud/metrics/internal/metadata"
	"github.com/tidbcloud/metrics/internal/otel_collector"
	"go.opentelemetry.io/collector/component"
	"go.opentelemetry.io/collector/config"
	"go.opentelemetry.io/collector/consumer"
	"go.opentelemetry.io/collector/model/pdata"
	"go.opentelemetry.io/collector/receiver/receiverhelper"
	"go.opentelemetry.io/collector/receiver/scraperhelper"
	"go.uber.org/zap"
)

const (
	typeStr = "metadb"
)

type Config struct {
	scraperhelper.ScraperControllerSettings `mapstructure:",squash"`
	Integration                             Integration `mapstructure:"integration"`
}

type Integration struct {
	ProviderTypes []string `mapstructure:"provider_type"`
}

func NewFactory() component.ReceiverFactory {
	return receiverhelper.NewFactory(
		typeStr,
		createDefaultConfig,
		receiverhelper.WithMetrics(createMetricsReceiver))
}

func createDefaultConfig() config.Receiver {
	return &Config{
		ScraperControllerSettings: scraperhelper.ScraperControllerSettings{
			ReceiverSettings: config.NewReceiverSettings(config.NewComponentID(typeStr)),
			// https://github.com/pingcap/tidb-operator/blob/master/charts/tidb-cluster/templates/config/_prometheus-config.tpl
			CollectionInterval: 15 * time.Second,
		},
		Integration: Integration{
			ProviderTypes: []string{"DATADOG"},
		},
	}
}

func createMetricsReceiver(
	_ context.Context,
	params component.ReceiverCreateSettings,
	rConf config.Receiver,
	consumer consumer.Metrics,
) (component.MetricsReceiver, error) {
	cfg := rConf.(*Config)

	// We only support Datadog integration for now.
	iTypes := make([]entity.IntegrationType, 0)
	for _, providerType := range cfg.Integration.ProviderTypes {
		if providerType != "DATADOG" {
			return nil, fmt.Errorf("unsupported integration type: %s", providerType)
		}
		iTypes = append(iTypes, entity.IntegrationType(providerType))
	}

	ns := newMetadbScraper(params.Logger, cfg, iTypes)
	scraper, err := scraperhelper.NewScraper(typeStr, ns.scrape)
	if err != nil {
		return nil, err
	}

	return scraperhelper.NewScraperControllerReceiver(
		&cfg.ScraperControllerSettings, params, consumer,
		scraperhelper.AddScraper(scraper),
	)
}

type metadbScraper struct {
	logger           *zap.Logger
	config           *Config
	integrationTypes []entity.IntegrationType
}

func newMetadbScraper(logger *zap.Logger, config *Config, iTypes []entity.IntegrationType) *metadbScraper {
	return &metadbScraper{
		logger:           logger,
		config:           config,
		integrationTypes: iTypes,
	}
}

func (m *metadbScraper) scrape(ctx context.Context) (pdata.Metrics, error) {
	// Get ids of projects whose integration type is matched and status is active.
	integrations, err := getIntegrations(m.integrationTypes)
	if err != nil {
		return pdata.Metrics{}, err
	}
	if len(integrations) < 1 {
		return pdata.Metrics{}, fmt.Errorf("no integration found, thus no metrics will be scraped")
	}
	integrationMap, prjIDs := parseIntegrations(integrations)
	// Get cluster by project ids.
	cs, err := getClusters(prjIDs)
	if err != nil {
		return pdata.Metrics{}, err
	}

	md := pdata.NewMetrics()

	// For each cluster, build a standalone ResourceMetric.
	for _, c := range cs {
		// Otel metric initialization.
		rs := md.ResourceMetrics().AppendEmpty()
		ilm := rs.InstrumentationLibraryMetrics().AppendEmpty()
		ilm.InstrumentationLibrary().SetName("otel/tidbcloud/metadb")
		now := pdata.NewTimestampFromTime(time.Now())

		memSlice := ilm.Metrics().AppendEmpty()
		metadata.M.TidbCloudNodeMemoryCapacityBytes.Init(memSlice)
		cpuSlice := ilm.Metrics().AppendEmpty()
		metadata.M.TidbCloudNodeCPUCapacityCores.Init(cpuSlice)
		diskSlice := ilm.Metrics().AppendEmpty()
		metadata.M.TidbCloudNodeStorageCapacityBytes.Init(diskSlice)
		id := strconv.FormatUint(c.ID.Uint64(), 10)
		prjID := strconv.FormatUint(c.ProjectID.Uint64(), 10)

		// Save cluster ids and datadog api keys in the resource attribute map.
		// These attributes are used by downstream pipeline.
		attrs := rs.Resource().Attributes()
		attrs.InsertString(otel_collector.ProjectIDKey, prjID)
		attrs.InsertString(otel_collector.ClusterIDKey, id)
		attrs.InsertString(otel_collector.ApiKeyKey, integrationMap[c.ProjectID].EncryptedDatadogApiKey)
		attrs.InsertString(otel_collector.SiteKey, integration.GetDatadogSiteUrl(integration.SiteDBToRest(integrationMap[c.ProjectID].DatadogSite)))
		attrs.InsertString(otel_collector.ClusterNameKey, c.Name)

		// Get cluster's components.
		comps, err := clustermodel.GetClusterComponentsV2(c)
		if err != nil {
			return pdata.Metrics{}, err
		}
		compMap := make(map[string]struct {
			mem  int64
			cpu  float64
			disk int64
		})
		for _, comp := range comps {
			if comp.ComponentName.String() == tidbcomp.PD.String() {
				continue
			}
			name := comp.ComponentName.String()
			compMap[name] = struct {
				mem  int64
				cpu  float64
				disk int64
			}{
				mem:  int64(comp.Capacity.Compute.MemoryMi) * 1024 * 1024,
				cpu:  float64(comp.Capacity.Compute.CPU),
				disk: int64(comp.Capacity.Disk.SizeGi) * 1024 * 1024 * 1024,
			}
		}

		// Get cluster's instances.
		thanosRes, err := central_thanos.QueryNodeMemoryUsedBytes(ctx, c.ID.Uint64())
		if err != nil || thanosRes.Status != "success" {
			return pdata.Metrics{}, fmt.Errorf("failed to query used node memory: %s", thanosRes)
		}

		// For each instance, create memory/cpu/disk capacity metrics.
		for _, result := range thanosRes.Data.Result {
			m := result.LabelSet
			valueTuple := result.Value
			if len(valueTuple) != 2 {
				return pdata.Metrics{}, fmt.Errorf("failed to query value(%+v) of used node memory", valueTuple)
			}
			if m.Component == tidbcomp.PD.String() {
				continue
			}
			resource := compMap[m.Component]

			writeIntDatapoint(memSlice, resource.mem, now, c.Name, id, m.Component, datasource.UnifyDBName(m.Instance))
			writeDoubleDatapoint(cpuSlice, resource.cpu, now, c.Name, id, m.Component, datasource.UnifyDBName(m.Instance))
			writeIntDatapoint(diskSlice, resource.disk, now, c.Name, id, m.Component, datasource.UnifyDBName(m.Instance))
		}
	}

	return md, nil
}

// Add a single data point and label set to the metric family(pdata.Metric).
func writeIntDatapoint(m pdata.Metric, value int64, t pdata.Timestamp, clusterName, clusterID, component, instance string) {
	labels := pdata.NewAttributeMap()
	labels.Insert(metadata.A.ClusterName, pdata.NewAttributeValueString(clusterName))
	labels.Insert(metadata.A.ClusterID, pdata.NewAttributeValueString(clusterID))
	labels.Insert(metadata.A.Component, pdata.NewAttributeValueString(component))
	labels.Insert(metadata.A.Instance, pdata.NewAttributeValueString(datasource.UnifyDBName(instance)))
	p := m.Gauge().DataPoints().AppendEmpty()
	p.SetTimestamp(t)
	p.SetIntVal(value)
	labels.CopyTo(p.Attributes())
}

func writeDoubleDatapoint(m pdata.Metric, value float64, t pdata.Timestamp, clusterName, clusterID, component, instance string) {
	labels := pdata.NewAttributeMap()
	labels.Insert(metadata.A.ClusterName, pdata.NewAttributeValueString(clusterName))
	labels.Insert(metadata.A.ClusterID, pdata.NewAttributeValueString(clusterID))
	labels.Insert(metadata.A.Component, pdata.NewAttributeValueString(component))
	labels.Insert(metadata.A.Instance, pdata.NewAttributeValueString(datasource.UnifyDBName(instance)))
	p := m.Gauge().DataPoints().AppendEmpty()
	p.SetTimestamp(t)
	p.SetDoubleVal(value)
	labels.CopyTo(p.Attributes())
}

// parseIntegrations transforms integrations to a datadog api key map and a project id list.
func parseIntegrations(is []entity.Integration) (map[entity.ID]entity.Integration, []entity.ID) {
	m := make(map[entity.ID]entity.Integration, len(is))
	l := make([]entity.ID, 0, len(is))
	for _, i := range is {
		m[i.ProjectID] = i
		l = append(l, i.ProjectID)
	}
	return m, l
}
