package service

import (
	"context"
	httpregion "dataservices/commons/http"
	"dataservices/commons/utils"
	dto "dataservices/internal/dto/dataapi"
	cdto "dataservices/internal/dto/dataapi_control"
	"dataservices/internal/entity"
	"dataservices/internal/repository"
	"sort"
	"sync"
	"time"

	pmodel "github.com/prometheus/common/model"
	errors "github.com/tidbcloud/tiinf/errs"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"go.uber.org/zap"
)

func GetDataAppExtIdAndRegions(ctx context.Context, appId uint64) (string, []string, error) {
	var wg sync.WaitGroup
	var (
		cres []dto.AppClusterInfo
		err1 error
		app  *entity.DataApp
		err2 error
	)
	wg.Add(2)
	utils.GO(func() {
		defer wg.Done()
		cres, err1 = ListAppCluster(ctx, dto.ListAppClusterReq{
			DataappID: appId,
		})
	})
	utils.GO(func() {
		defer wg.Done()
		app, err2 = repository.GetDataAppWithCtx(ctx, appId)
	})
	wg.Wait()
	if err1 != nil {
		tilog.Error(ctx, "ListAppCluster failed", zap.Error(err1))
		return "", nil, err1
	}
	if err2 != nil {
		tilog.Error(ctx, "GetDataApp failed", zap.Error(err2))
		return "", nil, err2
	}
	seen := make(map[string]bool)
	rs := []string{}
	for _, c := range cres {
		if !seen[c.RegionCode] {
			seen[c.RegionCode] = true
			rs = append(rs, c.RegionCode)
		}
	}
	return app.ExternalAppID, rs, nil
}

func GetDataAppEndpointsStatic(ctx context.Context, app uint64) (draft int64, pubed int64, err error) {
	tot, err := repository.GetAppEndpointCount(app)
	if err != nil {
		return 0, 0, err
	}
	pubed, err = repository.GetAppPublishedEndpointCount(app)
	if err != nil {
		return 0, 0, err
	}
	if tot < pubed {
		return 0, pubed, nil
	}
	return tot - pubed, pubed, nil
}

func MapMetricsQueryByRegions(ctx context.Context, req *cdto.PromQueryReq, dataapp uint64, regions []string) ([]*cdto.PromQueryRes, error) {
	if len(regions) == 0 {
		return nil, nil
	}
	errs := make([]error, len(regions))
	res := make([]*cdto.PromQueryRes, len(regions))
	var wg sync.WaitGroup
	for i := range regions {
		idx := i
		r := regions[i]
		wg.Add(1)
		utils.GO(func() {
			defer wg.Done()
			res[idx], errs[idx] = httpregion.QueryRegionMetrics(ctx, r, dataapp, req)
		})
	}
	wg.Wait()
	for i, e := range errs {
		if e != nil {
			tilog.Error(ctx, "query failed", zap.String("region", regions[i]), zap.Error(e))
			return nil, errors.NewInternalServerError("query failed", e)
		}
	}
	return res, nil
}

type reducrOp func(a, b pmodel.SampleValue) pmodel.SampleValue

func ReducrOpAdd(a, b pmodel.SampleValue) pmodel.SampleValue {
	return a + b
}

func ReducrOpMax(a, b pmodel.SampleValue) pmodel.SampleValue {
	if a < b {
		return b
	}
	return a
}

func ReduceMetricsRegionResult(input []*cdto.PromQueryRes, startTs, endTs, stepSec int64, op reducrOp) *cdto.PromQueryRes {
	output := cdto.PromQueryRes{
		Status: "success",
		Data: cdto.PromQueryData{
			ResultType: "matrix",
			Result:     []cdto.PromStream{{}},
		},
	}
	start := startTs * 1000
	end := endTs * 1000
	step := stepSec * 1000
	joinMap := make(map[pmodel.Time]pmodel.SampleValue)
	for i := start; i <= end; i += step {
		joinMap[pmodel.Time(i)] = 0.0
	}
	//tilog.Info(context.Background(), "ReduceMetricsRegionResult expand", zap.Any("map", joinMap))
	for _, item := range input {
		if len(item.Data.Result) == 0 {
			continue
		}
		//each item should have only one metric
		for _, v := range item.Data.Result[0].Values {
			ov := joinMap[v.Timestamp]
			joinMap[v.Timestamp] = op(ov, v.Value)
		}
	}
	for t, v := range joinMap {
		output.Data.Result[0].Values = append(output.Data.Result[0].Values, pmodel.SamplePair{
			Timestamp: t,
			Value:     v,
		})
	}
	sort.Slice(output.Data.Result[0].Values, func(i, j int) bool {
		return output.Data.Result[0].Values[i].Timestamp < output.Data.Result[0].Values[j].Timestamp
	})
	return &output
}

func QueryDataappMetricsReqCount(ctx context.Context, req cdto.PromQueryReq, appId uint64, regions []string) (*cdto.PromQueryRes, error) {
	if len(regions) == 0 {
		return &cdto.PromQueryRes{}, nil
	}
	req.QueryTopic = cdto.ConstMetricsQueryTopicCountReq
	res, err := MapMetricsQueryByRegions(ctx, &req, appId, regions)
	if err != nil {
		tilog.Error(ctx, "queryDataappMetricsReqCount failed", zap.Error(err))
		return nil, err
	}
	step := int64(time.Duration(req.Step) / time.Second)
	return ReduceMetricsRegionResult(res, int64(req.Start), int64(req.End), step, ReducrOpAdd), nil
}

func QueryDataappMetricsErrorCount(ctx context.Context, req cdto.PromQueryReq, appId uint64, regions []string) (*cdto.PromQueryRes, error) {
	if len(regions) == 0 {
		return &cdto.PromQueryRes{}, nil
	}
	req.QueryTopic = cdto.ConstMetricsQueryTopicCountError
	res, err := MapMetricsQueryByRegions(ctx, &req, appId, regions)
	if err != nil {
		tilog.Error(ctx, "queryDataappMetricsErrorCount failed", zap.Error(err))
		return nil, err
	}
	step := int64(time.Duration(req.Step) / time.Second)
	return ReduceMetricsRegionResult(res, int64(req.Start), int64(req.End), step, ReducrOpAdd), nil
}

func QueryDataappMetricsLantencySum(ctx context.Context, req cdto.PromQueryReq, appId uint64, regions []string) (*cdto.PromQueryRes, error) {
	if len(regions) == 0 {
		return &cdto.PromQueryRes{}, nil
	}
	req.QueryTopic = cdto.ConstMetricsQueryTopicSumLatency
	res, err := MapMetricsQueryByRegions(ctx, &req, appId, regions)
	if err != nil {
		tilog.Error(ctx, "queryDataappMetricsLantencySum failed", zap.Error(err))
		return nil, err
	}
	step := int64(time.Duration(req.Step) / time.Second)
	return ReduceMetricsRegionResult(res, int64(req.Start), int64(req.End), step, ReducrOpAdd), nil
}

func QueryDataappMetricsUnauthCount(ctx context.Context, req cdto.PromQueryReq, appId uint64, regions []string) (*cdto.PromQueryRes, error) {
	if len(regions) == 0 {
		return &cdto.PromQueryRes{}, nil
	}
	req.QueryTopic = cdto.ConstMetricsQueryTopicCountUnauth
	res, err := MapMetricsQueryByRegions(ctx, &req, appId, regions)
	if err != nil {
		tilog.Error(ctx, "queryDataappMetricsUnauthCount failed", zap.Error(err))
		return nil, err
	}
	step := int64(time.Duration(req.Step) / time.Second)
	return ReduceMetricsRegionResult(res, int64(req.Start), int64(req.End), step, ReducrOpAdd), nil
}

func QueryDataappMetricsLatencyPT99(ctx context.Context, req cdto.PromQueryReq, appId uint64, regions []string) (*cdto.PromQueryRes, error) {
	if len(regions) == 0 {
		return &cdto.PromQueryRes{}, nil
	}
	req.QueryTopic = cdto.ConstMetricsQueryTopicPT99Latency
	res, err := MapMetricsQueryByRegions(ctx, &req, appId, regions)
	if err != nil {
		tilog.Error(ctx, "queryDataappMetricsLatencyPT99 failed", zap.Error(err))
		return nil, err
	}
	step := int64(time.Duration(req.Step) / time.Second)
	return ReduceMetricsRegionResult(res, int64(req.Start), int64(req.End), step, ReducrOpMax), nil
}

func ReduceMetricsDiv(a, b *cdto.PromQueryRes) *cdto.PromQueryRes {
	output := cdto.PromQueryRes{
		Status: "success",
		Data: cdto.PromQueryData{
			ResultType: "matrix",
			Result:     []cdto.PromStream{{}},
		},
	}
	if len(b.Data.Result) == 0 && len(a.Data.Result) == 0 {
		return &output
	}

	amap := make(map[pmodel.Time]pmodel.SampleValue)
	if len(a.Data.Result) > 0 {
		for _, item := range a.Data.Result[0].Values {
			amap[item.Timestamp] = item.Value
		}
	}

	bmap := make(map[pmodel.Time]pmodel.SampleValue)
	if len(b.Data.Result) > 0 {
		for _, item := range b.Data.Result[0].Values {
			bmap[item.Timestamp] = item.Value
		}
	}
	joinMap := make(map[pmodel.Time]pmodel.SampleValue)
	// interset
	for ats, aval := range amap {
		bval, ok := bmap[ats]
		if ok && bval > 0.0 {
			joinMap[ats] = aval / bval
		} else {
			joinMap[ats] = .0
		}
	}
	for bts := range bmap {
		_, ok := joinMap[bts]
		if !ok {
			joinMap[bts] = .0
		}
	}
	for t, v := range joinMap {
		output.Data.Result[0].Values = append(output.Data.Result[0].Values, pmodel.SamplePair{
			Timestamp: t,
			Value:     v,
		})
	}
	sort.Slice(output.Data.Result[0].Values, func(i, j int) bool {
		return output.Data.Result[0].Values[i].Timestamp < output.Data.Result[0].Values[j].Timestamp
	})
	return &output
}

func QueryDataappMetricsTopKReq(ctx context.Context, req cdto.PromQueryReq, appId uint64, regions []string) (*cdto.PromQueryRes, error) {
	if len(regions) == 0 {
		return &cdto.PromQueryRes{}, nil
	}
	req.QueryTopic = cdto.ConstMetricsQueryTopicTopKReq
	res, err := MapMetricsQueryByRegions(ctx, &req, appId, regions)
	if err != nil {
		tilog.Error(ctx, "queryDataappMetricsTopKReq failed", zap.Error(err))
		return nil, err
	}
	if len(res) == 1 {
		return res[0], nil
	}
	return topkMetricsRegionResult(res, req.TopK, sameMetricValueMergeSum), nil
}

func QueryDataappMetricsTopKError(ctx context.Context, req cdto.PromQueryReq, appId uint64, regions []string) (*cdto.PromQueryRes, error) {
	if len(regions) == 0 {
		return &cdto.PromQueryRes{}, nil
	}
	req.QueryTopic = cdto.ConstMetricsQueryTopicTopKError
	res, err := MapMetricsQueryByRegions(ctx, &req, appId, regions)
	if err != nil {
		tilog.Error(ctx, "queryDataappMetricsTopKError failed", zap.Error(err))
		return nil, err
	}
	if len(res) == 1 {
		return res[0], nil
	}
	return topkMetricsRegionResult(res, req.TopK, sameMetricValueMergeSum), nil
}

func QueryDataappMetricsTopKLatency(ctx context.Context, req cdto.PromQueryReq, appId uint64, regions []string) (*cdto.PromQueryRes, error) {
	if len(regions) == 0 {
		return &cdto.PromQueryRes{}, nil
	}
	req.QueryTopic = cdto.ConstMetricsQueryTopicTopKLantency
	res, err := MapMetricsQueryByRegions(ctx, &req, appId, regions)
	if err != nil {
		tilog.Error(ctx, "queryDataappMetricsTopKLatency failed", zap.Error(err))
		return nil, err
	}
	if len(res) == 1 {
		return res[0], nil
	}
	return topkMetricsRegionResult(res, req.TopK, sameMetricValueMergeAvg), nil
}

type sameMetricValueMerge func(a, b pmodel.SampleValue) pmodel.SampleValue

func sameMetricValueMergeSum(a, b pmodel.SampleValue) pmodel.SampleValue {
	return a + b
}

func sameMetricValueMergeAvg(a, b pmodel.SampleValue) pmodel.SampleValue {
	return (a + b) / 2.0
}

func topkMetricsRegionResult(input []*cdto.PromQueryRes, topk uint64, act sameMetricValueMerge) *cdto.PromQueryRes {
	output := cdto.PromQueryRes{
		Status: "success",
		Data: cdto.PromQueryData{
			ResultType: "vector",
			Result:     []cdto.PromStream{},
		},
	}
	metrics := make(map[string]pmodel.SamplePair)
	for _, item := range input {
		if len(item.Data.Result) == 0 {
			continue
		}
		for i := range item.Data.Result {
			endpointId := item.Data.Result[i].Metric["endpoint_id"]
			if len(endpointId) == 0 {
				continue
			}
			v := item.Data.Result[i].Value
			if pv, ok := metrics[endpointId]; ok {
				metrics[endpointId] = pmodel.SamplePair{
					Timestamp: pv.Timestamp,
					Value:     act(pv.Value, v.Value)}
			} else {
				metrics[endpointId] = v
			}

		}
	}
	temp := []*cdto.PromStream{}
	for endpointId, v := range metrics {
		temp = append(temp, &cdto.PromStream{
			Metric: map[string]string{"endpoint_id": endpointId},
			Value:  v,
		})
	}
	sort.Slice(temp, func(i, j int) bool { return temp[i].Value.Value > temp[j].Value.Value })
	for i := 0; i < len(temp) && i < int(topk); i++ {
		output.Data.Result = append(output.Data.Result, cdto.PromStream{
			Metric: temp[i].Metric,
			Value:  temp[i].Value,
		})
	}
	return &output
}

func QueryDataappMetricsCurrentErrorRate(ctx context.Context, appId uint64, extId string, regions []string) (float64, error) {
	if len(regions) == 0 {
		return 0.0, nil
	}
	end := time.Now()
	var req = cdto.PromQueryReq{
		DataAppExtId:  extId,
		Start:         uint64(end.Unix() - 1),
		End:           uint64(end.Unix()),
		Step:          pmodel.Duration(5 * time.Minute),
		RangeSelector: pmodel.Duration(5 * time.Minute),
	}
	metricsReqData, err := QueryDataappMetricsReqCount(ctx, req, appId, regions)
	if err != nil {
		tilog.Error(ctx, "QueryDataappMetricsErrorRate failed!", zap.Error(err))
		return 0.0, err
	}
	metricsErrorData, err := QueryDataappMetricsErrorCount(ctx, req, appId, regions)
	if err != nil {
		tilog.Error(ctx, "QueryDataappMetricsErrorCount failed!", zap.Error(err))
		return 0.0, err
	}
	metricsdata := ReduceMetricsDiv(metricsErrorData, metricsReqData)
	if len(metricsdata.Data.Result) == 0 || len(metricsdata.Data.Result[0].Values) == 0 {
		return .0, nil
	}
	sample := metricsdata.Data.Result[0].Values[len(metricsdata.Data.Result[0].Values)-1]
	return float64(sample.Value), nil
}
