package datasource

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"io/ioutil"
	"net/http"
	"sync"
	"time"

	"github.com/pingcap/log"
	"github.com/tidbcloud/metrics-server/central/config"
	"github.com/tidbcloud/metrics-server/central/db/entity"
	"github.com/tidbcloud/metrics-server/central/metrics"
	"github.com/tidbcloud/metrics-server/central/monitor_data_migration"
	"github.com/tidbcloud/pkgv2/auth/auth0"
	"go.uber.org/zap"
)

type MetricDataAddress struct {
	IntegrationType string
	TidbClusterInfo TidbClusterInfo
	QueryUrl        string
	IsThanos        bool
}

type metricDataAddresses struct {
	addresses map[entity.ID]MetricDataAddress
	lock      sync.RWMutex
}

func (addrs *metricDataAddresses) set(tidbClusterID entity.ID, addr MetricDataAddress) {
	addrs.lock.Lock()
	defer addrs.lock.Unlock()

	addrs.addresses[tidbClusterID] = addr
}

func (addrs *metricDataAddresses) get(tidbClusterID entity.ID) (MetricDataAddress, bool) {
	addrs.lock.RLock()
	defer addrs.lock.RUnlock()

	addr, ok := addrs.addresses[tidbClusterID]
	return addr, ok
}

type integrationMetricDataAddresses struct {
	integrationType string
	addresses       metricDataAddresses
}

const (
	datadogIntegrationType   string = "datadog"
	thirdPromIntegrationType string = "thirdProm"
)

var (
	datadogIntegrationTidbClusterMetricDataAddresses = integrationMetricDataAddresses{
		integrationType: datadogIntegrationType,
		addresses: metricDataAddresses{
			addresses: map[entity.ID]MetricDataAddress{},
			lock:      sync.RWMutex{},
		},
	}

	thirdPromIntegrationTidbClusterMetricDataAddresses = integrationMetricDataAddresses{
		integrationType: thirdPromIntegrationType,
		addresses: metricDataAddresses{
			addresses: map[entity.ID]MetricDataAddress{},
			lock:      sync.RWMutex{},
		},
	}
)

func (iaddrs *integrationMetricDataAddresses) setMetricDataAddress(tidbClusterID entity.ID, addr MetricDataAddress) {
	defer func() {
		log.Debug("setMetricDataAddress result",
			zap.String("integrationType", iaddrs.integrationType),
			zap.Uint64("tidb_cluster_id", tidbClusterID.Uint64()),
			zap.String("QueryUrl", addr.QueryUrl),
			zap.Bool("IsThanos", addr.IsThanos))
	}()

	iaddrs.addresses.set(tidbClusterID, addr)
}

func (iaddrs *integrationMetricDataAddresses) getMetricDataAddress(tidbClusterID entity.ID) (addr MetricDataAddress, exists bool) {
	defer func() {
		log.Info("getMetricDataAddress result",
			zap.String("integrationType", iaddrs.integrationType),
			zap.Uint64("tidb_cluster_id", tidbClusterID.Uint64()),
			zap.String("QueryUrl", addr.QueryUrl),
			zap.Bool("IsThanos", addr.IsThanos),
			zap.Bool("addr_exists", exists))
	}()

	return iaddrs.addresses.get(tidbClusterID)
}

type TidbClusterInfo struct {
	Vendor    string
	Region    string
	TenantID  entity.ID
	ProjectID entity.ID
	ClusterID entity.ID
}

func (iaddrs *integrationMetricDataAddresses) getAndSetMetricDataAddressWithTidbClusterInfo(ctx context.Context, projectMigrationStatus monitor_data_migration.ProjectMigrationStatus, info TidbClusterInfo) (addr MetricDataAddress, err error) {
	defer func() {
		if err == nil {
			iaddrs.setMetricDataAddress(info.ClusterID, addr)
		}

		log.Info("getAndSetMetricDataAddressWithTidbClusterInfo result",
			zap.String("integrationType", iaddrs.integrationType),
			zap.Bool("projectMigrationStatus", projectMigrationStatus.IsMigrationInProgress()),
			zap.String("tidb_cluster_vendor", info.Vendor),
			zap.String("tidb_cluster_region", info.Region),
			zap.Uint64("tidb_cluster_tenant_id", info.TenantID.Uint64()),
			zap.Uint64("tidb_cluster_project_id", info.ProjectID.Uint64()),
			zap.Uint64("tidb_cluster_id", info.ClusterID.Uint64()),
			zap.String("IntegrationType", addr.IntegrationType),
			zap.String("QueryUrl", addr.QueryUrl),
			zap.Bool("IsThanos", addr.IsThanos),
			zap.Error(err))
	}()

	addr, exists := iaddrs.getMetricDataAddress(info.ClusterID)
	if !exists || addr.IsThanos {
		queryUrl, err := getMetricDataQueryUrlWithRetry(ctx, info)
		if err != nil {
			log.Error("getAndSetMetricDataAddressWithTidbClusterInfo call getMetricDataQueryUrlWithRetry fail",
				zap.String("integrationType", iaddrs.integrationType),
				zap.Bool("projectMigrationStatus", projectMigrationStatus.IsMigrationInProgress()),
				zap.String("tidb_cluster_vendor", info.Vendor),
				zap.String("tidb_cluster_region", info.Region),
				zap.Uint64("tidb_cluster_tenant_id", info.TenantID.Uint64()),
				zap.Uint64("tidb_cluster_project_id", info.ProjectID.Uint64()),
				zap.Uint64("tidb_cluster_id", info.ClusterID.Uint64()),
				zap.String("queryUrl", queryUrl),
				zap.Error(err))
			return MetricDataAddress{}, err
		}

		addr = MetricDataAddress{
			IntegrationType: iaddrs.integrationType,
			TidbClusterInfo: info,
			QueryUrl:        queryUrl,
			IsThanos:        false,
		}
	}

	return addr, nil
}

type queryClustersResult struct {
	Code    int               `json:"code"`    //http error code
	Message string            `json:"message"` //http error msg
	Data    queryClustersResp `json:"data"`    //The returned content, storage in JSON format
}

type queryClustersResp struct {
	Clusters           []clusterBaseInfo `json:"clusters"`
	ContinueToken      string            `json:"continueToken" form:"continueToken"`
	RemainingItemCount int64             `json:"remainingItemCount" form:"remainingItemCount"`
}

type clusterBaseInfo struct {
	ID            string `json:"id" form:"id"`
	Vendor        string `json:"vendor" form:"vendor"`
	Region        string `json:"region" form:"region"`
	TenantID      string `json:"tenantID" form:"tenantID"`
	ProjectID     string `json:"projectID" form:"projectID"`
	ApplicationID string `json:"applicationID" form:"applicationID"`
	BizType       string `json:"bizType" form:"bizType"`

	Name      string   `json:"name" form:"name"`
	Labels    []string `json:"labels" form:"labels"`
	Version   string   `json:"version" form:"version"`
	ShareMode string   `json:"shareMode" form:"shareMode"`
	Note      string   `json:"note" form:"note"`
}

var errNoMetricDataClusterFound = errors.New("get no metric clusters for tidb cluster from o11y global server")

func getMetricDataQueryUrlWithRetry(ctx context.Context, info TidbClusterInfo) (queryUrl string, err error) {
	defer func() {
		if err != nil {
			log.Error("getMetricDataQueryUrlWithRetry process fail",
				zap.String("tidb_cluster_vendor", info.Vendor),
				zap.String("tidb_cluster_region", info.Region),
				zap.Uint64("tidb_cluster_tenant_id", info.TenantID.Uint64()),
				zap.Uint64("tidb_cluster_project_id", info.ProjectID.Uint64()),
				zap.Uint64("tidb_cluster_id", info.ClusterID.Uint64()),
				zap.Error(err),
			)
		}
	}()

	reqCnt := 3
	for i := 0; i < reqCnt; i++ {
		queryUrl, canRetry, err := getMetricDataQueryUrl(ctx, info)
		if err == nil {
			return queryUrl, nil
		} else {
			log.Warn("getMetricDataQueryUrl failed",
				zap.String("tidb_cluster_vendor", info.Vendor),
				zap.String("tidb_cluster_region", info.Region),
				zap.Uint64("tidb_cluster_tenant_id", info.TenantID.Uint64()),
				zap.Uint64("tidb_cluster_project_id", info.ProjectID.Uint64()),
				zap.Uint64("tidb_cluster_id", info.ClusterID.Uint64()),
				zap.Error(err),
			)
		}

		if i == reqCnt-1 {
			return "", err
		}

		if canRetry {
			log.Info("retry getMetricDataQueryUrl",
				zap.String("tidb_cluster_vendor", info.Vendor),
				zap.String("tidb_cluster_region", info.Region),
				zap.Uint64("tidb_cluster_tenant_id", info.TenantID.Uint64()),
				zap.Uint64("tidb_cluster_project_id", info.ProjectID.Uint64()),
				zap.Uint64("tidb_cluster_id", info.ClusterID.Uint64()),
			)
			time.Sleep(100 * time.Millisecond * time.Duration(i+1))
			continue
		}

		return "", err
	}

	return "", errors.New("unknown error for get metric data query url with retry")
}

func getMetricDataQueryUrl(ctx context.Context, info TidbClusterInfo) (queryUrl string, canRetry bool, err error) {
	o11yGlobalServerUrl := config.Global().O11yGlobalServerConfig.Url
	o11yBizType := config.Global().O11yGlobalServerConfig.BizType
	path := "/api/v1/clusters/"
	urlQuery := fmt.Sprintf("vendor=%s&region=%s&tenantID=%d&projectID=%d&applicationID=%d&bizType=%s",
		info.Vendor, info.Region, info.TenantID.Uint64(), info.ProjectID.Uint64(), info.ClusterID.Uint64(), o11yBizType)
	urlString := fmt.Sprintf("%s%s?%s", o11yGlobalServerUrl, path, urlQuery)

	startTime := time.Now()
	method := "GET"
	defer func() {
		duration := float64(time.Since(startTime)) / float64(time.Millisecond)
		metrics.O11yServerRequestDuration.WithLabelValues(o11yGlobalServerUrl, path, method).Observe(duration)
		if err != nil {
			metrics.O11yServerRequestErrCnt.WithLabelValues(o11yGlobalServerUrl, path, method).Inc()
		}

		log.Info("getMetricDataQueryUrl result",
			zap.String("urlString", urlString),
			zap.String("queryUrl", queryUrl),
			zap.Error(err),
		)
	}()

	req, err := http.NewRequestWithContext(ctx, method, urlString, nil)
	if err != nil {
		return "", false, err
	}

	token, err := auth0.GetClientToken(
		config.Global().O11yGlobalServerConfig.Auth0Domain,
		config.Global().O11yGlobalServerConfig.Auth0ClientID,
		config.Global().O11yGlobalServerConfig.Auth0ClientSecret,
		config.Global().O11yGlobalServerConfig.Auth0Audience)
	if err != nil {
		return "", true, err
	}
	req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token))

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		return "", true, err
	}
	defer resp.Body.Close() //nolint

	if resp.StatusCode != http.StatusOK {
		body, _ := ioutil.ReadAll(resp.Body)
		return "", false, fmt.Errorf("http response status %s from o11y global server, body: %s", resp.Status, body)
	}

	result := queryClustersResult{}
	if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
		return "", false, err
	}

	if result.Code != 0 {
		return "", false, fmt.Errorf("o11y global server process fail, result code: %d, message: %s", result.Code, result.Message)
	}

	metricDataClustersLen := len(result.Data.Clusters)
	if metricDataClustersLen == 0 {
		return "", false, errNoMetricDataClusterFound
	}

	if metricDataClustersLen > 1 {
		return "", false, fmt.Errorf("get %d metric clusters for tidb cluster from o11y global server", metricDataClustersLen)
	}

	metricDataClusterID := result.Data.Clusters[0].ID
	if len(metricDataClusterID) == 0 {
		return "", false, errors.New("metric cluster id is empty or not exists for tidb cluster from o11y global server")
	}

	queryUrl = fmt.Sprintf("https://www.ds.%s.%s.%s%s/%s",
		info.Region, info.Vendor,
		config.Global().O11yGlobalServerConfig.DomainSuffix,
		config.Global().O11yGlobalServerConfig.RegionalDataServerUriPrefix,
		metricDataClusterID)

	return queryUrl, false, nil
}

type metricDataAddress struct{}

var (
	metricDataAddressKey metricDataAddress
)

func SetMetricDataAddressToContextWithTidbClusterInfo(ctx context.Context, projectMigrationStatus monitor_data_migration.ProjectMigrationStatus, info TidbClusterInfo) (context.Context, error) {
	addr, err := datadogIntegrationTidbClusterMetricDataAddresses.getAndSetMetricDataAddressWithTidbClusterInfo(ctx, projectMigrationStatus, info)
	if err != nil {
		return ctx, err
	}

	return context.WithValue(ctx, metricDataAddressKey, addr), nil
}

func SetMetricDataAddressToContextWithCache(ctx context.Context, tidbClusterID entity.ID) (context.Context, error) {
	addr, ok := datadogIntegrationTidbClusterMetricDataAddresses.getMetricDataAddress(tidbClusterID)
	if !ok {
		return ctx, errors.New("addr not exists in cache")
	}

	if len(addr.QueryUrl) == 0 {
		return ctx, errors.New("invalid metric_data_addr in cache")
	}

	return context.WithValue(ctx, metricDataAddressKey, addr), nil
}

func SetMetricDataAddressToContextWithTidbClusterInfoV2(ctx context.Context, projectMigrationStatus monitor_data_migration.ProjectMigrationStatus, info TidbClusterInfo) (context.Context, error) {
	addr, err := thirdPromIntegrationTidbClusterMetricDataAddresses.getAndSetMetricDataAddressWithTidbClusterInfo(ctx, projectMigrationStatus, info)
	if err != nil {
		return ctx, err
	}

	return context.WithValue(ctx, metricDataAddressKey, addr), nil
}

func GetMetricDataAddressFromContext(ctx context.Context) (addr MetricDataAddress, err error) {
	if ctx == nil {
		return MetricDataAddress{}, errors.New("ctx is nil")
	}

	value := ctx.Value(metricDataAddressKey)
	if value == nil {
		return MetricDataAddress{}, errors.New("no metric_data_addr in context")
	}

	addr, ok := value.(MetricDataAddress)
	if !ok || len(addr.QueryUrl) == 0 {
		return MetricDataAddress{}, errors.New("invalid metric_data_addr in context")
	}

	return addr, nil
}
