package service

import (
	"context"
	"fmt"
	"math"
	"slices"
	"strings"
	"time"

	"github.com/duke-git/lancet/v2/slice"

	confMetrics "github.com/pingcap-inc/clinic/clinic_data_service/pkg/config/metrics"
	"github.com/pingcap-inc/clinic/clinic_data_service/pkg/dto"
	"github.com/pingcap-inc/clinic/clinic_data_service/pkg/log"
	"github.com/pingcap-inc/clinic/clinic_data_service/pkg/prometheus"
	"github.com/pingcap-inc/clinic/clinic_data_service/pkg/utils"
	"github.com/prometheus/common/model"
	"go.uber.org/zap"
)

var MetricUnits = map[string]string{
	"latency":     "ms",
	"cpu_tidb":    "%",
	"cpu_tikv":    "%",
	"memory_tidb": "%",
	"memory_tikv": "%",
}
var oneMinMetrics = []string{
	"qps", "cps", "qps_all",
}

// the metrics that are compared at the node level (use DataPlaneMetricsURL + $namespace)
var nodeMetrics = []string{
	"iops", "iops_exceed_max", "iops_exceed_avg",
	"memory_tikv", "memory_tidb",
	"component_num",
}

// userScore metrics: premium 集群也使用 ClusterId 作为 tidb_cluster_id
var userScoreMetrics = []string{
	"latency_p999_all", "latency_p999",
	 "latency_p99_all", "latency_p99", 
	 "latency_p95_all", "latency_p95", 
	 "latency_avg_all","latency_p999_by_type",
	 "latency_avg_by_type",
	"cpu_tidb", "cps","affected_rows_by_type",
	"qps_all","qps_by_type",
}

var jsonTagAll = []string{
	"iops", "iops_exceed_max", "iops_exceed_avg", "",
}

// the max ,min  in json
var formattedValueMap = map[string]string{
	"default":         "%.2f",
	"iops_exceed_max": "%.8f",
	"iops_exceed_avg": "%.8f",
}

type MetricsDiffApiService struct {
}

func NewMetricsDiffApiService() *MetricsDiffApiService {
	return &MetricsDiffApiService{}
}

type ClusterMetricsComparator struct {
	Result      *dto.ClusterData
	DiffMetrics []dto.DiffMetric
}

func getClusterMetricsComparator(resultDataProd, resultDataTest model.Value, queryName string) *ClusterMetricsComparator {
	clusterDataDict := &dto.ClusterData{
		QueryName: queryName,
		ClusterSet: dto.ClusterSet{
			ClusterA: resultDataProd,
			ClusterB: resultDataTest,
		},
	}
	return &ClusterMetricsComparator{
		Result: clusterDataDict,
	}
}

func filterCompareQueriesByDiffKeys(compareQueries []confMetrics.MetricsQueryConfig, diffKeys []string) ([]confMetrics.MetricsQueryConfig, error) {
	if len(diffKeys) == 0 {
		return compareQueries, nil
	}

	diffKeysMap := make(map[string]bool)
	for _, key := range diffKeys {
		diffKeysMap[key] = true
	}

	filteredQueries := make([]confMetrics.MetricsQueryConfig, 0)
	for _, queryConfig := range compareQueries {
		if diffKeysMap[queryConfig.Name] {
			filteredQueries = append(filteredQueries, queryConfig)
		}
	}

	if len(filteredQueries) == 0 {
		return nil, fmt.Errorf("no matching compare queries found for diffKeys: %v", diffKeys)
	}

	return filteredQueries, nil
}

func (s *MetricsDiffApiService) CompareClusterPerformance(ctx context.Context, req *dto.CompareRequest) (*dto.MetricsCompareResponse, error) {
	if req == nil {
		return nil, fmt.Errorf("request cannot be nil")
	}
	ctx, cancel := context.WithTimeout(ctx, 20*time.Minute) //
	defer cancel()

	// 1.1 Create Cluster Prometheus clients
	//pcClusterA, err := prometheus.CreatePrometheusClientTiDB(req.ProdClusterID)
	// pcClusterA, err := prometheus.CreatePrometheusClient(ctx, prometheus.CreatePrometheusClientReq{ClusterID: req.ProdClusterID})

	pcClusterA, err := prometheus.CreatePrometheusClientWithUrls(ctx, req.ClusterA.ClusterUrls.ClusterMetricsURL)
	if err != nil {
		log.Error(ctx, "Failed to create prod Prometheus client", zap.Error(err), zap.String("clusterId", req.ClusterA.ClusterId))
		return nil, fmt.Errorf("failed to create prod Prometheus client: %w", err)
	}

	log.Info(ctx, "get cluster prometheus clients", zap.Any("clusterB", req.ClusterB.ClusterId))
	// pcClusterB, err := prometheus.CreatePrometheusClient(ctx, prometheus.CreatePrometheusClientReq{ClusterID: req.TestClusterID})

	pcClusterB, err := prometheus.CreatePrometheusClientWithUrls(ctx, req.ClusterB.ClusterUrls.ClusterMetricsURL)
	if err != nil {
		log.Error(ctx, "Failed to create test Prometheus client", zap.Error(err), zap.String("clusterId", req.ClusterB.ClusterId))
		return nil, fmt.Errorf("failed to create test Prometheus client: %w", err)
	}
	log.Info(ctx, "get cluster prometheus clients", zap.Any("clusterA", req.ClusterA.ClusterId))

	
	pcNodeClusterA, err := prometheus.CreatePrometheusClientWithUrls(ctx, req.ClusterA.ClusterUrls.DataPlaneMetricsURL)
	if err != nil {
		log.Error(ctx, "Failed to create prod Prometheus client", zap.Error(err), zap.String("clusterId", req.ClusterA.ClusterId))
		return nil, fmt.Errorf("failed to create prod Prometheus client: %w", err)
	}
	log.Info(ctx, "get node prometheus clients", zap.Any("clusterB", req.ClusterB.ClusterId))
	pcNodeClusterB, err := prometheus.CreatePrometheusClientWithUrls(ctx, req.ClusterB.ClusterUrls.DataPlaneMetricsURL)
	if err != nil {
		log.Error(ctx, "Failed to create test Prometheus client", zap.Error(err), zap.String("clusterId", req.ClusterB.ClusterId))
		return nil, fmt.Errorf("failed to create test Prometheus client: %w", err)
	}

	// 2. Get query configurations
	compareQueries := confMetrics.AppConfig.GetCompareQuery()
	if len(compareQueries) == 0 {
		return nil, fmt.Errorf("no compare queries configured")
	}

	// 3. Filter query configurations based on DiffKeys
	compareQueries, err = filterCompareQueriesByDiffKeys(compareQueries, req.DiffKeys)
	if err != nil {
		return nil, err
	}
	log.Info(ctx, "compare queries", zap.Any("metric size", len(compareQueries)))
	// 4. Set default values
	interval := req.Interval
	if interval == "" {
		interval = "2m"
	}

	// Parse Step from Interval to keep Step and Interval consistent
	step, err := time.ParseDuration(interval)
	if err != nil {
		return nil, fmt.Errorf("invalid interval format: %s, error: %w", interval, err)
	}

	// 5. Iterate through each query configuration and perform comparison
	var allDiffResults []dto.DiffResult
	for _, queryConfig := range compareQueries {
		// log.Info(ctx, "Querying metrics", zap.String(queryConfig.Name, queryConfig.PrimaryQuery))
		metricName := queryConfig.Name
		if slice.Contain(oneMinMetrics, metricName) {
			interval = "1m"
			step = 1 * time.Minute
		}
		query := strings.ReplaceAll(queryConfig.PrimaryQuery, "$interval", interval)
		log.Info(ctx, "", zap.String(metricName, query))
		queryTest, queryProd := query, query
		var clientA, clientB *prometheus.PrometheusClient
		// node/dataplane metrics: use DataPlaneMetricsURL + replace $namespace
		if slice.Contain(nodeMetrics, metricName) {
			log.Info(ctx, "get node prometheus clients", zap.Any("clusterA", req.ClusterA.ClusterId))
			clientA = pcNodeClusterA
			clientB = pcNodeClusterB

			// $namespace: 非premium用ClusterId，premium用PhysicalClusterID
			aNamespaceId := req.ClusterA.ClusterId
			bNamespaceId := req.ClusterB.ClusterId
			if req.ClusterA.DeployType == "premium" && req.ClusterA.PhysicalClusterID != "" {
				aNamespaceId = req.ClusterA.PhysicalClusterID
			}
			if req.ClusterB.DeployType == "premium" && req.ClusterB.PhysicalClusterID != "" {
				bNamespaceId = req.ClusterB.PhysicalClusterID
			}

			queryProd = strings.ReplaceAll(query, "$namespace", aNamespaceId)
			queryTest = strings.ReplaceAll(query, "$namespace", bNamespaceId)
		} else {
			// cluster-level metrics: use ClusterMetricsURL + replace $tidb_cluster
			clientA = pcClusterA
			clientB = pcClusterB

			// $tidb_cluster: 非premium用ClusterId，premium用PhysicalClusterID
			// 特殊：userScore指标（如cpu_tidb）premium也用ClusterId
			aTidbCluster := req.ClusterA.ClusterId
			bTidbCluster := req.ClusterB.ClusterId
			if !slice.Contain(userScoreMetrics, metricName) {
				if req.ClusterA.DeployType == "premium" && req.ClusterA.PhysicalClusterID != "" {
					aTidbCluster = req.ClusterA.PhysicalClusterID
				}
				if req.ClusterB.DeployType == "premium" && req.ClusterB.PhysicalClusterID != "" {
					bTidbCluster = req.ClusterB.PhysicalClusterID
				}
			}

			queryProd = strings.ReplaceAll(query, "$tidb_cluster", aTidbCluster)
			queryTest = strings.ReplaceAll(query, "$tidb_cluster", bTidbCluster)
		}

		// Extend context timeout for long-running PromQL queries
		// ctxWithTimeout, cancel := context.WithTimeout(ctx, 10*time.Minute)
		// defer cancel()

		// Normalize time zones to UTC for cross-cluster comparison
		startTimeUTC := req.ClusterA.TimeRange.Start.UTC()
		endTimeUTC := req.ClusterA.TimeRange.End.UTC()
		// prod
		ctxProd, cancelProd := context.WithTimeout(ctx, 10*time.Minute)
		prodStart := time.Now()
		resultProd, err := clientA.QueryRange(ctxProd, queryProd, startTimeUTC, endTimeUTC, step)
		cancelProd()
		log.Info(ctx, "prod query finished",
			zap.String("metric", metricName),
			zap.Duration("cost", time.Duration(time.Since(prodStart).Minutes())),
		)
		if err != nil {
			log.Error(ctx, "QueryRange failed for prod cluster",
				zap.Error(err),
				zap.String("queryName", queryConfig.Name),
				zap.String("clusterId", req.ClusterA.ClusterId))
			return nil, fmt.Errorf("queryRange failed for prod query %s: %w", queryConfig.Name, err)
		}

		// Normalize time zones to UTC for cross-cluster comparison
		startTimeUTC = req.ClusterB.TimeRange.Start.UTC()
		endTimeUTC = req.ClusterB.TimeRange.End.UTC()
		ctxTest, cancelTest := context.WithTimeout(ctx, 10*time.Minute)
		testStart := time.Now()
		resultTest, err := clientB.QueryRange(ctxTest, queryTest, startTimeUTC, endTimeUTC, step)
		cancelTest()
		log.Info(ctx, "test query finished",
			zap.String("metric", metricName),
			zap.Duration("cost", time.Since(testStart)),
		)
		if err != nil {
			log.Error(ctx, "QueryRange failed for test cluster",
				zap.Error(err),
				zap.String("queryName", queryConfig.Name),
				zap.String("clusterId", req.ClusterB.ClusterId))
			return nil, err
		}

		if resultTest == nil || resultProd == nil {
			log.Warn(ctx, "Query result is empty",
				zap.String("queryName", queryConfig.Name))
			continue
		}

		comparator := getClusterMetricsComparator(resultProd, resultTest, queryConfig.Name)
		comparator.filterMatchingTypes()
		comparator.calculateAggregation()

		if req.CsvDir != "" {
			// Always generate CSV files
			for _, diffMetric := range comparator.DiffMetrics {
				var prodCSVPath, testCSVPath string
				if strings.ToLower(diffMetric.Tag) == "all" {
					prodCSVPath = fmt.Sprintf("%s/prod_%s.csv", req.CsvDir, strings.ToLower(queryConfig.Name))
					testCSVPath = fmt.Sprintf("%s/replay_%s.csv", req.CsvDir, strings.ToLower(queryConfig.Name))
				} else {
					prodCSVPath = fmt.Sprintf("%s/prod_%s_%s.csv", req.CsvDir, strings.ToLower(queryConfig.Name), strings.ToLower(diffMetric.Tag))
					testCSVPath = fmt.Sprintf("%s/replay_%s_%s.csv", req.CsvDir, strings.ToLower(queryConfig.Name), strings.ToLower(diffMetric.Tag))
				}

				if err := utils.WritePointDataForTestToCSV(prodCSVPath, testCSVPath, diffMetric); err != nil {
					log.Warn(ctx, "Failed to write CSV files",
						zap.Error(err),
						zap.String("queryName", queryConfig.Name),
						zap.String("type", diffMetric.Tag))
				} else {
					log.Info(ctx, "Generated CSV files",
						zap.String("queryName", queryConfig.Name),
						zap.String("type", diffMetric.Tag),
						zap.String("prodPath", prodCSVPath),
						zap.String("testPath", testCSVPath))
				}
			}
		}

		diffResult := dto.DiffResult{
			Key:     queryConfig.Name,
			Metrics: comparator.DiffMetrics,
		}
		allDiffResults = append(allDiffResults, diffResult)
	}
	response := &dto.MetricsCompareResponse{
		DiffClusters: dto.ClusterDiffInfoTest{
			ClusterA: req.ClusterA.ClusterId,
			ClusterB: req.ClusterB.ClusterId,
		},
		ClusterDiff: allDiffResults,
		Units:       []string{"latency", "ms"},
		Result:      dto.Result{IsSuccess: true},
	}

	return response, nil
}

func extractTagFromMetric(metric model.Metric, queryName string) string {
	if len(metric) == 0 {
		return "All"
	}

	// Since there's only one element, get the first (and only) label value
	var tag string
	for _, v := range metric {
		if v != "" {
			tag = string(v)
			break
		}
	}

	if slices.Contains(jsonTagAll, queryName) {
		return "All"
	}

	// Apply normalization rules for client_data_traffic
	if queryName == "client_data_traffic" {
		if tag == "In" {
			return "read"
		} else if tag == "Out" {
			return "write"
		}
	}

	return tag
}

func formatValueForDiff(queryName string, value float64) string {
	if math.IsNaN(value) || math.IsInf(value, 0) {
		return "N/A"
	}

	// if queryName == "latency_p999" || queryName == "latency_p99" || queryName == "latency_p95" || queryName == "latency_avg" {
	// 	value = value * 1000
	// }

	// Format based on queryName
	// switch queryName {
	// case "IOPS":
	// 	return fmt.Sprintf("%.4f", value)
	// case "latency_p999", "latency_p99", "latency_p95", "latency_avg":
	// 	return fmt.Sprintf("%.1f", value)
	// case "cps":
	// 	return fmt.Sprintf("%d", int(value))
	// case "Storage_TiKV", "Storage_TiFlash":
	// 	return fmt.Sprintf("%.2f", value)
	// case "error_rate":
	// 	return fmt.Sprintf("%.5f", value)
	// default:
	// 	return fmt.Sprintf("%.2f", value)
	// }

	if queryName == "cps" {
		return fmt.Sprintf("%d", int(value))
	}
	formatStr, ok := formattedValueMap[queryName]
	if !ok {
		formatStr = formattedValueMap["default"]
	}
	return fmt.Sprintf(formatStr, value)
}

// func calculateDiffString(valA, valB float64) string {
// 	if valA == 0 {
// 		if valB == 0 {
// 			return "0.00"
// 		}
// 		return "N/A"
// 	}
// 	percent := utils.CalculateDiff(valB,valA)

// 	return fmt.Sprintf("%s", percent)

// 	// ratio := 1 - (valB / valA)
// 	// return fmt.Sprintf("%.2f", ratio)
// }

func (c *ClusterMetricsComparator) filterMatchingTypes() {
	if c.Result == nil {
		c.DiffMetrics = []dto.DiffMetric{}
		return
	}

	resultTest := c.Result.ClusterSet.ClusterB
	resultProd := c.Result.ClusterSet.ClusterA
	queryName := c.Result.QueryName

	matrixTest, ok := resultTest.(model.Matrix)
	if !ok {
		c.DiffMetrics = []dto.DiffMetric{}
		return
	}

	matrixProd, ok := resultProd.(model.Matrix)
	if !ok {
		c.DiffMetrics = []dto.DiffMetric{}
		return
	}

	testTypeMap := make(map[string]*model.SampleStream)
	for i := range matrixTest {
		stream := matrixTest[i]
		tag := extractTagFromMetric(stream.Metric, queryName)
		testTypeMap[tag] = stream
	}

	c.DiffMetrics = make([]dto.DiffMetric, 0, len(testTypeMap))

	for testTag, testStream := range testTypeMap {
		prodTag := testTag

		// Find matching prod stream
		var foundProdStream *model.SampleStream
		for i := range matrixProd {
			stream := matrixProd[i]
			prodStreamTag := extractTagFromMetric(stream.Metric, queryName)
			if prodStreamTag == prodTag {
				foundProdStream = stream
				break
			}
		}

		if foundProdStream != nil {
			pointValues := make([]dto.DiffPoint, 0)

			// Get the minimum length to avoid index out of range
			maxLen := len(testStream.Values)
			if len(foundProdStream.Values) < maxLen {
				maxLen = len(foundProdStream.Values)
			}

			// Check if all values are zero
			allZero := true

			for i := 0; i < maxLen; i++ {
				testSample := testStream.Values[i]
				prodSample := foundProdStream.Values[i]

				// timeValA := time.Unix(int64(prodSample.Timestamp)/1000, 0).UTC()
				timeValA := time.UnixMilli(int64(prodSample.Timestamp)).In(utils.GetUtf8Location())
				timeStrA := timeValA.Format("2006-01-02 15:04:05")

				// timeValB := time.Unix(int64(testSample.Timestamp)/1000, 0).UTC()
				timeValB := time.UnixMilli(int64(testSample.Timestamp)).In(utils.GetUtf8Location())
				timeStrB := timeValB.Format("2006-01-02 15:04:05")
				testVal := math.Round(float64(testSample.Value)*10000) / 10000
				prodVal := math.Round(float64(prodSample.Value)*10000) / 10000

				if testVal != 0 || prodVal != 0 {
					allZero = false
				}

				pointValue := dto.DiffPoint{
					TimeA:                 timeStrA,
					ClusterA:              prodVal,
					TimeB:                 timeStrB,
					ClusterB:              testVal,
					ClusterAUnixTimestamp: int64(prodSample.Timestamp) / 1000,
					ClusterBUnixTimestamp: int64(testSample.Timestamp) / 1000,
				}
				pointValues = append(pointValues, pointValue)
			}

			if allZero {
				continue
			}

			// Add result if we have any values
			if len(pointValues) > 0 {
				c.DiffMetrics = append(c.DiffMetrics, dto.DiffMetric{
					Tag:    testTag,
					Points: pointValues,
				})
			}
		}
	}
}

func (c *ClusterMetricsComparator) calculateAggregation() {
	if c.Result == nil {
		return
	}

	queryName := c.Result.QueryName
	if queryName == "qps_by_type" {
		log.PlainInfo(queryName+": ", zap.Any("DiffMetrics len", len(c.DiffMetrics)))
	}

	for i := range c.DiffMetrics {
		diffMetric := &c.DiffMetrics[i]

		if len(diffMetric.Points) == 0 {
			diffMetric.Aggregation = dto.DiffAggregation{}
			continue
		}

		var clusterAValues []float64
		var clusterBValues []float64

		for _, point := range diffMetric.Points {
			if !math.IsNaN(point.ClusterA) && !math.IsInf(point.ClusterA, 0) {
				clusterAValues = append(clusterAValues, point.ClusterA)
			}
			if !math.IsNaN(point.ClusterB) && !math.IsInf(point.ClusterB, 0) {
				clusterBValues = append(clusterBValues, point.ClusterB)
			}
		}
		log.PlainInfo(queryName+":clusterA: ", zap.Any("clusterAValues len", len(clusterAValues)))
		log.PlainInfo(queryName+":clusterB: ", zap.Any("clusterBValues len", len(clusterBValues)))

		// Calculate Max and Avg for ClusterA
		var clusterAMax, clusterAAvg float64
		if len(clusterAValues) > 0 {
			clusterAMax = clusterAValues[0]
			sum := 0.0
			for _, val := range clusterAValues {
				if val > clusterAMax {
					clusterAMax = val
				}
				sum += val
			}
			clusterAAvg = sum / float64(len(clusterAValues))
		}

		var clusterBMax, clusterBAvg float64
		if len(clusterBValues) > 0 {
			clusterBMax = clusterBValues[0]
			sum := 0.0
			for _, val := range clusterBValues {
				if val > clusterBMax {
					clusterBMax = val
				}
				sum += val
			}
			clusterBAvg = sum / float64(len(clusterBValues))
		}

		// Format values based on queryName
		clusterAMaxStr := formatValueForDiff(queryName, clusterAMax)
		clusterAAvgStr := formatValueForDiff(queryName, clusterAAvg)
		clusterBMaxStr := formatValueForDiff(queryName, clusterBMax)
		clusterBAvgStr := formatValueForDiff(queryName, clusterBAvg)

		maxDiff := utils.CalculateDiff(clusterBMax, clusterAMax)
		avgDiff := utils.CalculateDiff(clusterBAvg, clusterAAvg)
		// log.PlainInfo(queryName+":maxDiff: ", zap.Any("maxDiff", *maxDiff))
		// log.PlainInfo(queryName+":avgDiff: ", zap.Any("avgDiff", *avgDiff))
		maxResp := &dto.DiffAggregationValue{
			ClusterA: clusterAMaxStr,
			ClusterB: clusterBMaxStr,
			// Diff:     *maxDiff,
		}
		if maxDiff != nil {
			maxResp.Diff = *maxDiff
		}
		avgResp := &dto.DiffAggregationValue{
			ClusterA: clusterAAvgStr,
			ClusterB: clusterBAvgStr,
			// Diff:     *avgDiff,
		}
		if avgDiff != nil {
			avgResp.Diff = *avgDiff
		}

		diffMetric.Aggregation = dto.DiffAggregation{
			Max: maxResp,
			Avg: avgResp,
		}
	}
}
