package prometheus

import (
	"context"
	"fmt"
	"math"
	"sort"
	"time"

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

const (
	DurationThreshold = 3 //min

)

// TimeWindow defines a time range with a start and end time.
type TimeWindow struct {
	Start            time.Time
	End              time.Time
	WindowMaxLatency float64
}

type TikvGrpcLatencyResp struct {
	TikvLatencyBeans []*TikvLatencyBean
	ClusterId        string
}
type TikvLatencyBean struct {
	Instance               string
	MaxLatencyWindow       TimeWindow
	LatencyWindowTopDigest map[TimeWindow][]*DigestInfo
	LatencyWindows         []LatencyWindow
}
type LatencyWindow struct {
	LatencyWindow TimeWindow
	// LatencyWindow        TimeWindow
	InstanceLatency        float64
	TopDigests             []DigestInfo
	InstanceMaxCpu         float64
	InstanceMaxCpuDiff     float64
	ClusterLatencyP95      float64
	ClusterLatencyP95Diff  float64
	ClusterLatencyP99      float64
	ClusterLatencyP99Diff  float64
	ClusterLatencyP999     float64
	ClusterLatencyP999Diff float64
}

// ClusterLatency encapsulates cluster latency percentiles in milliseconds.
type ClusterLatency struct {
	P95  float64
	P99  float64
	P999 float64
}

/*
	    unit: ms
		curl -G \
		  'https://www.ds.us-east-1.aws.observability.tidbcloud.com/internal/metrics/ed869d81-91c5-4a87-a28d-20795c738d3f/api/v1/query' \
		  --data-urlencode 'query=histogram_quantile(0.999, sum(rate(tidb_server_handle_query_duration_seconds_bucket{tidb_cluster_id="1379661944646416027",instance=~"db-tidb-.*"}[1d])) by (le))' \
		  --data-urlencode 'time=2025-09-23T00:00:00Z' | jq -r
*/
func GetClusterLatencyDay(clusterId string, dayStr string) (float64, error) {
	query := fmt.Sprintf(`1000 *histogram_quantile(0.999, sum(rate(tidb_server_handle_query_duration_seconds_bucket{tidb_cluster_id="%s",instance=~"db-tidb-.*"}[1d])) by (le))`, clusterId)
	return GetOneValueByDay(clusterId, dayStr, query)

}
func (pc *PrometheusClient) GetClusterLatency(clusterId string, percentile float64, queryTime time.Time, interval string) (float64, error) {
	query := fmt.Sprintf(`1000 *histogram_quantile(%f, sum(rate(tidb_server_handle_query_duration_seconds_bucket{tidb_cluster_id="%s",instance=~"db-tidb-.*"}[%s])) by (le))`, percentile, clusterId, interval)
	latencyVal, err := pc.GetOneValueByTime(clusterId, queryTime, query)

	if err != nil {
		log.Error(context.Background(), "Failed to get cluster latency", zap.Error(err))
		return 0, err
	}
	if percentile == 0.999 {
		log.Info(context.Background(), "0.999 latency", zap.Float64("latencyVal", latencyVal))
	}
	latencyVal = utils.RoundFloat(latencyVal)
	return latencyVal, nil

}

// setClusterLatencyAll computes P95, P99, P999 for a cluster at a given time and interval.
func setClusterLatencyAll(pc *PrometheusClient, clusterId string, interval string, latencyWindow *LatencyWindow, historyWindowMap map[string][]TimeWindow) error {

	// startTime := latencyWindow.LatencyWindow.Start
	queryTime := latencyWindow.LatencyWindow.End

	// get diff windows
	diffWindows := getDiffTimeWindows(clusterId, historyWindowMap, latencyWindow.LatencyWindow)

	diffQueryTime := diffWindows.End

	previousWinP95, err := pc.GetClusterLatency(clusterId, 0.95, diffQueryTime, interval)
	if err != nil {
		return err
	}
	p95, err := pc.GetClusterLatency(clusterId, 0.95, queryTime, interval)
	if err != nil {
		return err
	}

	previousWinP99, err := pc.GetClusterLatency(clusterId, 0.99, diffQueryTime, interval)
	if err != nil {
		return err
	}
	p99, err := pc.GetClusterLatency(clusterId, 0.99, queryTime, interval)
	if err != nil {
		return err
	}

	previousWinP999, err := pc.GetClusterLatency(clusterId, 0.999, diffQueryTime, interval)
	if err != nil {
		return err
	}
	p999, err := pc.GetClusterLatency(clusterId, 0.999, queryTime, interval)
	if err != nil {
		return err
	}
	log.Info(context.Background(), "", zap.Float64("previousWinP999", previousWinP999), zap.Float64("p999", p999))
	latencyWindow.ClusterLatencyP95 = p95
	latencyWindow.ClusterLatencyP95Diff = *utils.CalculateDiff(p95, previousWinP95)
	latencyWindow.ClusterLatencyP99 = p99
	latencyWindow.ClusterLatencyP99Diff = *utils.CalculateDiff(p99, previousWinP99)
	latencyWindow.ClusterLatencyP999 = p999
	latencyWindow.ClusterLatencyP999Diff = *utils.CalculateDiff(p999, previousWinP999)

	return nil
}

func getDiffTimeWindows(clusterId string, historyWindowMap map[string][]TimeWindow, LatencyWindow TimeWindow) TimeWindow {
	diffWindows := TimeWindow{
		Start: LatencyWindow.Start.Add(-24 * time.Hour),
		End:   LatencyWindow.End.Add(-24 * time.Hour),
	}
	historyWindows := historyWindowMap[clusterId]
	for {
		hasIntersect := false
		for _, historyWindow := range historyWindows {
			// 判断是否有交集
			if !(historyWindow.End.Before(diffWindows.Start) || historyWindow.Start.After(diffWindows.End)) {
				hasIntersect = true
				break
			}
		}
		if hasIntersect {
			// 有交集则往前移24小时，继续判断
			diffWindows.Start = diffWindows.Start.Add(-24 * time.Hour)
			diffWindows.End = diffWindows.End.Add(-24 * time.Hour)
		} else {
			// 没有交集则返回
			break
		}
	}
	return diffWindows
}

// func GetTikvGrpcLatency(clusterId string) (*TikvGrpcLatencyResp, error) {

// 	// For development and testing, use a fixed time range.
// 	layout := "2006-01-02 15:04:05"
// 	startTimeStr := "2025-10-15 02:00:00" // UTC time
// 	startTime, err := time.Parse(layout, startTimeStr)
// 	if err != nil {
// 		log.PlainError("failed to parse time", zap.Error(err))
// 		return nil, err
// 	}
// 	endTime := startTime.Add(30 * time.Minute)
// 	return GetTikvGrpcLatencyByTime(clusterId, startTime, endTime)
// }

func (pc *PrometheusClient) GetTikvGrpcLatencyByTime(clusterId string, startTime, endTime time.Time, historyWindowMap map[string][]TimeWindow) (*TikvGrpcLatencyResp, error) {
	resp := &TikvGrpcLatencyResp{
		TikvLatencyBeans: []*TikvLatencyBean{},
	}
	threshold := 1000.0

	// 1. Call getTikvGrpcLatencyByRange to get instances with latency exceeding the threshold.
		instanceMap, err := pc.getTikvGrpcLatencyByRange(clusterId, startTime, endTime, threshold)
		if err != nil {
		log.Error(context.Background(), "getTikvGrpcLatencyByRange failed", zap.Error(err))
			return nil, err
		}

	// cluster := config.GetClusterByID(clusterId)
	cluster, err := common.GetCluster(context.Background(), clusterId)
		if err != nil {
		log.Error(context.Background(), "Failed to get cluster", zap.String("cluster", clusterId), zap.Error(err))
			return nil, err
		}

	if len(instanceMap) == 0 {
		log.Info(context.Background(), fmt.Sprintf("[%s]: No instances found with latency above threshold[%fms]", cluster.Alias, threshold))
		return &TikvGrpcLatencyResp{}, nil
	}

	for instance, samplePairs := range instanceMap {
		if len(samplePairs) == 0 {
			continue
		}

		// 2. For each instance, merge the time windows.
		mergedWindows, maxLatencyWindow := mergeTimeWindows(samplePairs)
		log.Info(context.Background(), "Merged windows for cluster"+cluster.Alias, zap.String("instance", instance), zap.Any("windows", mergedWindows))

		// 3. For each merged window, get the cluster latency,MaxCpu and top 5 CPU digests.
		// var topDigests []DigestInfo
		windowTopDigestMap := map[TimeWindow][]*DigestInfo{}
		latencyWindowArr := []LatencyWindow{}
		// var maxLatency float64
		for _, window := range mergedWindows {
			latencyBean := LatencyWindow{
				LatencyWindow: window,
			}

			intervalDuration := window.End.Sub(window.Start)
			intervalStr := fmt.Sprintf("%ds", int(intervalDuration.Seconds()))

			// 3.1. get cluster latency
			err := setClusterLatencyAll(pc, clusterId, intervalStr, &latencyBean, historyWindowMap)
			if err != nil {
				log.Error(context.Background(), "GetLatency failed", zap.String("clusterId", clusterId), zap.Error(err))
				// todo 这里可以继续处理，例如记录日志或发送告警
				continue
			}

			// 3.2. get max cpu
			err = pc.setTikvMaxCpuAndDiff(clusterId, instance, &latencyBean)
			if err != nil {
				log.Error(context.Background(), "getTikvMaxCpuAndDiff failed", zap.String("clusterId", clusterId), zap.Error(err))
				continue
			}

			latencyWindowArr = append(latencyWindowArr, latencyBean)
			//db-tikv-1.db-tikv-peer.tidb1379661944646416027.svc:20160
			// promSqlInstance := instance + "\\..*"
			log.Info(context.Background(), "instance", zap.String("instance", instance))
			promSqlInstance := instance + `[.].*`
			log.Info(context.Background(), "promSqlInstance", zap.String("promSqlInstance", promSqlInstance))
			// 3.3. get top 5 cpu digests
			req := GetTopCpuTikvDigestsReq{
				ClusterId: clusterId,
				TopN:      5,
				QueryTime: window.End,
				Instance:  promSqlInstance,
				Interval:  intervalStr,
			}

			topDigests, err := pc.GetTopCpuTikvDigests(req)
			if err != nil {
				log.Error(context.Background(), "GetTopCpuTikvDigests failed",
					zap.String("instance", instance),
					zap.Any("window", window),
					zap.Error(err))
				continue // Continue to the next window
			}
			for i := 0; i < len(topDigests); i++ {
				cpuVal := topDigests[i].Value / 1000 / 60
				cpuVal = math.Round(cpuVal*100) / 100
				topDigests[i].Value = cpuVal
				digest := topDigests[i].Digest
				cpuPercent, err := pc.GetTikvCpuPercent(clusterId, window.End, digest, promSqlInstance, intervalStr)
				if err != nil {
					log.PlainError("GetTikvCpuPercent failed", zap.String("instance", instance), zap.String("clusterId", clusterId), zap.Error(err))
					continue // Continue to the next window
				}
				cpuPercent = utils.RoundFloat(cpuPercent, 2)
				topDigests[i].CpuPer = cpuPercent
			}
			if len(topDigests) > 0 {

				//  convert to pointer
				arr := make([]*DigestInfo, len(topDigests))
				for i := 0; i < len(topDigests); i++ {
					arr[i] = &topDigests[i]
				}

				windowTopDigestMap[window] = arr

				log.PlainInfo(clusterId+": Top 5 CPU digests",
					zap.String("instance", instance),
					zap.Any("window", window),
					zap.Any("digests", topDigests))
			}
		}

		tikvLatencyBean := &TikvLatencyBean{
			Instance:               instance,
			MaxLatencyWindow:       maxLatencyWindow,
			LatencyWindowTopDigest: windowTopDigestMap,
			LatencyWindows:         latencyWindowArr,
		}

		resp.TikvLatencyBeans = append(resp.TikvLatencyBeans, tikvLatencyBean)
	}
	resp.ClusterId = clusterId

	return resp, nil
}

// mergeTimeWindows takes a list of sample pairs, finds the one with the highest latency,
func mergeTimeWindows(samplePairs []model.SamplePair) ([]TimeWindow, TimeWindow) {
	if len(samplePairs) == 0 {
		return nil, TimeWindow{}
	}

	// Find the sample pair with the maximum latency value.
	maxLatencySp := samplePairs[0]
	var windows []TimeWindow
	for _, sp := range samplePairs {
		ts := sp.Timestamp.Time().UTC()
		latency := math.Round(float64(sp.Value)*100) / 100
		w := TimeWindow{
			Start:            ts.Add(-2 * time.Minute),
			End:              ts.Add(2 * time.Minute),
			WindowMaxLatency: latency,
		}
		now := time.Now().UTC()
		if w.End.After(now) {
			log.PlainInfo("window end time is after now, set to now", zap.Any("End", w.End), zap.Any("now", now))
			w.End = now
		}
		windows = append(windows, w)
		if sp.Value > maxLatencySp.Value {
			maxLatencySp = sp
		}
	}
	maxLatencyTimeWindow := TimeWindow{
		Start:            maxLatencySp.Timestamp.Time().Add(-2 * time.Minute).UTC(),
		End:              maxLatencySp.Timestamp.Time().UTC(),
		WindowMaxLatency: utils.RoundFloat(float64(maxLatencySp.Value), 0),
	}

	sort.Slice(windows, func(i, j int) bool {
		return windows[i].Start.Before(windows[j].Start)
	})

	var merged []TimeWindow
	if len(windows) > 0 {
		currentMerge := windows[0]
		for i := 1; i < len(windows); i++ {
			nextWindow := windows[i]
			if !nextWindow.Start.After(currentMerge.End) {
				if nextWindow.End.After(currentMerge.End) {
					currentMerge.End = nextWindow.End
				}
				if nextWindow.WindowMaxLatency > currentMerge.WindowMaxLatency {
					currentMerge.WindowMaxLatency = nextWindow.WindowMaxLatency
				}
			} else {
				merged = append(merged, currentMerge)
				currentMerge = nextWindow
			}
		}
		merged = append(merged, currentMerge)
	}
	log.PlainInfo("merged", zap.Any("merged", merged))
	// remove  interval that is too small
	var filtered []TimeWindow
	for _, window := range merged {
		if window.End.After(window.Start.Add(DurationThreshold * time.Minute)) {
			filtered = append(filtered, window)
		}
	}
	log.PlainInfo("remove small interval(5min)", zap.Any("filtered", filtered))
	// wlt test
	// filtered = merged
	return filtered, maxLatencyTimeWindow
}

/*
unit: ms
curl -G \
  'https://www.ds.us-east-1.aws.observability.tidbcloud.com/internal/metrics/bd692fb5-6454-464a-b40d-ca0d9f188c12/api/v1/query_range' \
  --data-urlencode 'query=histogram_quantile(0.99, sum(rate(tikv_grpc_msg_duration_seconds_bucket{tidb_cluster_id="1379661944646416076",instance=~"db-tikv-6.*",type!="kv_gc", type ="coprocessor"}[2m])) by (le, instance))' \
  --data-urlencode 'start=2025-09-05T04:00:00Z' \
  --data-urlencode 'end=2025-09-05T05:00:00Z' \
  --data-urlencode 'step=120s' | jq -r
*/

func (pc *PrometheusClient) getTikvGrpcLatencyByRange(clusterId string, startTime, endTime time.Time, threshold float64) (map[string][]model.SamplePair, error) {
	query := fmt.Sprintf(`1000*histogram_quantile(0.99, sum(rate(tikv_grpc_msg_duration_seconds_bucket{tidb_cluster_id="%s",instance=~"db-tikv-.*",type!="kv_gc", type ="coprocessor"}[2m])) by (le, instance))`, clusterId)
	step := 2 * time.Minute

	matrix, err := pc.QueryRangeMatrix(clusterId, query, startTime, endTime, step)
	if err != nil {
		return nil, err
	}

	highLatencyInstances := make(map[string][]model.SamplePair)

	for _, sampleStream := range matrix {
		instance := string(sampleStream.Metric["instance"])
		for _, samplePair := range sampleStream.Values {
			latency := float64(samplePair.Value) // ms

			if latency > threshold {
				highLatencyInstances[instance] = append(highLatencyInstances[instance], samplePair)
			}
		}
	}

	return highLatencyInstances, nil
}
