package datadog

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

	lru "github.com/hashicorp/golang-lru"
	ddconfig "github.com/open-telemetry/opentelemetry-collector-contrib/exporter/datadogexporter"
	dbaasConfig "github.com/tidbcloud/metrics-server/central/config"
	"github.com/tidbcloud/metrics-server/internal/otel_collector"
	"github.com/tidbcloud/pkgv2/secret/encrypt"
	secretprovider "github.com/tidbcloud/pkgv2/secret/provider"
	"go.opentelemetry.io/collector/component"
	"go.opentelemetry.io/collector/config"
	"go.opentelemetry.io/collector/config/confignet"
	"go.opentelemetry.io/collector/exporter/exporterhelper"
	"go.opentelemetry.io/collector/pdata/pmetric"
	"go.uber.org/zap"
)

const (
	typeStr                          = "multidatadog"
	defaultTenantCapacity            = 32
	errorTimeoutDuration             = time.Second * 60
	distributionAggregatedHistoryTTL = 3600
)

type Config struct {
	config.ExporterSettings        `mapstructure:",squash"`
	exporterhelper.TimeoutSettings `mapstructure:",squash"`
	QueueSettings                  exporterhelper.QueueSettings `mapstructure:"sending_queue"`
	exporterhelper.RetrySettings   `mapstructure:"retry_on_failure"`
}

func (cfg *Config) Validate() error {
	return cfg.QueueSettings.Validate()
}

func NewFactory() component.ExporterFactory {
	return component.NewExporterFactory(
		typeStr,
		createDefaultConfig,
		component.WithMetricsExporter(createMetricsExporter, component.StabilityLevelStable),
	)
}

func createDefaultConfig() config.Exporter {
	return &Config{
		ExporterSettings: config.NewExporterSettings(config.NewComponentID(typeStr)),
		TimeoutSettings:  exporterhelper.TimeoutSettings{Timeout: 15 * time.Second},
		RetrySettings:    exporterhelper.NewDefaultRetrySettings(),
		QueueSettings:    exporterhelper.NewDefaultQueueSettings(),
	}
}

func createMetricsExporter(
	_ context.Context,
	set component.ExporterCreateSettings,
	c config.Exporter,
) (component.MetricsExporter, error) {
	cfg := c.(*Config)

	// Init aws kms encryptor from dbaas config.
	cmkArn := dbaasConfig.Global().ServerConfig.CMKArn
	kmsEnc, err := secretprovider.NewSecretEncryptor(cmkArn)
	if err != nil {
		return nil, err
	}

	exp, err := newMultiDatadogExporter(set.Logger, cfg, kmsEnc, set)
	if err != nil {
		return nil, err
	}

	return exporterhelper.NewMetricsExporter(
		cfg,
		set,
		exp.PushMetrics,
		exporterhelper.WithTimeout(cfg.TimeoutSettings),
		exporterhelper.WithRetry(cfg.RetrySettings),
		exporterhelper.WithQueue(cfg.QueueSettings),
	)
}

type multiDatadogExporter struct {
	set             component.ExporterCreateSettings
	config          *Config
	tenantExporters *lru.Cache
	kmsEncryptor    encrypt.Encryptor

	// lastErrTime is used to record whether there is any error happened in each project id.
	lastErrTime map[string]time.Time
	mu          sync.Mutex

	logger *zap.Logger
}

func newMultiDatadogExporter(logger *zap.Logger, config *Config, kmsEncryptor encrypt.Encryptor, set component.ExporterCreateSettings) (*multiDatadogExporter, error) {
	cache, err := lru.New(defaultTenantCapacity)
	if err != nil {
		return nil, err
	}

	// Init error status from metadb.
	is, err := initErrorStatus()
	if err != nil {
		return nil, err
	}
	times := make(map[string]time.Time, defaultTenantCapacity)
	now := time.Now()
	for _, i := range is {
		prjID := strconv.FormatUint(i.ProjectID.Uint64(), 10)
		times[prjID] = now
	}

	exporter := &multiDatadogExporter{
		set:             set,
		config:          config,
		tenantExporters: cache,
		kmsEncryptor:    kmsEncryptor,
		logger:          logger,
		lastErrTime:     times,
	}
	return exporter, nil
}

func (e *multiDatadogExporter) PushMetrics(ctx context.Context, md pmetric.Metrics) error {
	// For each ResourceMetric in the pdata.Metrics:
	//   - Get the datadog api key from the resource attributes.
	//   - Send it to corresponding datadog client.
	for i := 0; i < md.ResourceMetrics().Len(); i++ {
		// Get the datadog api key from the resource attributes.
		rm := md.ResourceMetrics().At(i)
		attrs := rm.Resource().Attributes()
		_site, ok := attrs.Get(otel_collector.SiteKey)
		if !ok {
			e.logger.Warn("failed to get site from resource attributes", zap.Any("resource", attrs))
			continue
		}
		site := _site.StringVal()
		_apiKey, ok := attrs.Get(otel_collector.ApiKeyKey)
		if !ok {
			e.logger.Warn("failed to get api key from resource attributes", zap.Any("resource", attrs))
			continue
		}
		apiKey := _apiKey.StringVal()
		_prjID, ok := attrs.Get(otel_collector.ProjectIDKey)
		if !ok {
			e.logger.Warn("failed to get project id from resource attributes", zap.Any("resource", attrs))
			continue
		}
		prjID := _prjID.StringVal()
		_cName, ok := attrs.Get(otel_collector.ClusterNameKey)
		if !ok {
			e.logger.Warn("failed to get cluster name from resource attributes", zap.Any("resource", attrs))
			continue
		}
		cName := _cName.StringVal()
		_cId, ok := attrs.Get(otel_collector.ClusterIDKey)
		if !ok {
			e.logger.Warn("failed to get cluster id from resource attributes", zap.Any("resource", attrs))
			continue
		}
		cId := _cId.StringVal()

		// Get or create corresponding datadog client.
		if !e.tenantExporters.Contains(apiKey) {
			decryptedKey, err := encrypt.DecryptBase64(e.kmsEncryptor, apiKey)
			if err != nil {
				e.logger.Warn("decrypt datadog exporter failed", zap.Error(err))
				continue
			}
			exp, err := newSingleMetricsExporter(ctx, e.set, e.defaultDDExporterConfig(decryptedKey.String(), site))
			if err != nil {
				e.recordError(prjID, cName, err.Error())
				e.logger.Warn("create datadog exporter failed", zap.Error(err))
				continue
			}
			if err != nil {
				e.recordError(prjID, cName, err.Error())
				e.logger.Warn("start datadog exporter failed", zap.Error(err))
				continue
			}
			e.tenantExporters.Add(apiKey, exp)
		}
		exporter, _ := e.tenantExporters.Get(apiKey)

		// Create a standalone pdata.Metrics without upstream Resource object.
		newM := pmetric.NewMetrics()
		newIlm := newM.ResourceMetrics().AppendEmpty().ScopeMetrics()
		rm.ScopeMetrics().CopyTo(newIlm)

		if err := exporter.(*singleExporter).PushMetricsData(ctx, newM); err != nil {
			e.recordError(prjID, cName, err.Error())
			e.logger.Warn("failed to push metrics to datadog exporter", zap.String("cluster_id", cId), zap.Error(err), zap.Any("metricsData", newM))
			continue
		}

		e.logger.Info("push metrics to datadog exporter", zap.String("cluster_id", cId))
	}

	e.cleanupError()
	return nil
}

// defaultDDExporterConfig build datadog exporter config for each datadog api key.
// Each datadog api key has a standalone exporter, thus a standalone config.
func (e *multiDatadogExporter) defaultDDExporterConfig(ddApiKey, site string) *ddconfig.Config {
	exporterConf := config.NewExporterSettings(e.config.ID())
	exporterConf.SetIDName(ddApiKey)
	ddCfg := &ddconfig.Config{
		ExporterSettings: exporterConf,
		TimeoutSettings:  e.config.TimeoutSettings,
		RetrySettings:    e.config.RetrySettings,
		QueueSettings:    e.config.QueueSettings,
		API: ddconfig.APIConfig{
			Key:  ddApiKey,
			Site: site,
		},
		TagsConfig: ddconfig.TagsConfig{
			Hostname: otel_collector.DefaultHost,
		},
		Metrics: ddconfig.MetricsConfig{
			DeltaTTL: distributionAggregatedHistoryTTL,
			ExporterConfig: ddconfig.MetricsExporterConfig{
				ResourceAttributesAsTags:           false,
				InstrumentationScopeMetadataAsTags: false,
			},
			HistConfig: ddconfig.HistogramConfig{
				Mode:         "distributions",
				SendCountSum: false,
			},
			SumConfig: ddconfig.SumConfig{
				CumulativeMonotonicMode: ddconfig.CumulativeMonotonicSumModeToDelta,
			},
			SummaryConfig: ddconfig.SummaryConfig{
				Mode: ddconfig.SummaryModeGauges,
			},
			TCPAddr: confignet.TCPAddr{
				Endpoint: fmt.Sprintf("https://api.%s", site),
			},
		},
		Traces: ddconfig.TracesConfig{},
		HostMetadata: ddconfig.HostMetadataConfig{
			Enabled:        true,
			HostnameSource: ddconfig.HostnameSourceFirstResource,
		},
	}

	_ = ddCfg.Validate()

	return ddCfg
}

// recordError update exporter state and metadb record when error occurs.
func (e *multiDatadogExporter) recordError(projectID, clusterName, errorMsg string) {
	e.mu.Lock()
	defer e.mu.Unlock()
	err := updateErrorStatusInDB(projectID, clusterName, errorMsg)
	if err != nil {
		e.logger.Warn("failed to write error status to metadb", zap.Error(err))
		return
	}
	e.lastErrTime[projectID] = time.Now()
}

// cleanupError clean up exporter state and metadb record when error timeouts.
func (e *multiDatadogExporter) cleanupError() {
	e.mu.Lock()
	defer e.mu.Unlock()
	for prj, t := range e.lastErrTime {
		if time.Since(t) > errorTimeoutDuration {
			if err := cleanupErrorStatusInDB(prj); err != nil {
				e.logger.Warn("failed to cleanup error status in metadb", zap.Error(err))
				continue
			}
			delete(e.lastErrTime, prj)
		}
	}
}
