package cluster

import (
	"bytes"
	"context"
	"fmt"
	"math"
	"sort"
	"strconv"
	"strings"
	"time"

	"github.com/pingcap/errors"
	"github.com/pingcap/log"
	promcli "github.com/prometheus/client_golang/api/prometheus/v1"
	"github.com/prometheus/common/model"
	"go.uber.org/zap"
	"gorm.io/gorm"
)

type SLA struct {
	Value float64 `gorm:"type:float"`
}

func (o *SLA) TableName() string {
	return "sla"
}

func (s *ClusterService) GetSLA(clusterID string) float64 {
	if s.telemetryCli == nil {
		return math.NaN()
	}

	var resp SLA
	err := s.telemetryCli.Transaction(func(tx *gorm.DB) error {
		return tx.Where(fmt.Sprintf("cluster_id = '%s' and month='%s'", clusterID, fmt.Sprintf("%d/%d", time.Now().Year(), time.Now().Month()))).First(&resp).Error
	})
	if err != nil {
		log.Error("get sla failed", zap.String("cluster", clusterID), zap.Error(err))
		return math.NaN()
	}

	return resp.Value
}

func metricGroupByComponent(ctx context.Context, api promcli.API, query string) ([]MetricValue, error) {
	v, _, err := api.Query(ctx, query, time.Now())
	if err != nil {
		return nil, err
	}

	vector, ok := v.(model.Vector)
	if !ok {
		return nil, errors.New("expect vector type")
	}

	if vector.Len() < 1 {
		return nil, nil
	}

	results := make([]MetricValue, 0)
	for _, v := range vector {
		if com, ok := v.Metric[model.LabelName("component")]; ok {
			results = append(results, MetricValue{
				Component: string(com),
				Value:     uaDouble(v.Value),
			})
		}
	}

	return results, nil
}

func aggMetric(ctx context.Context, api promcli.API, query string) (float64, error) {
	v, _, err := api.Query(ctx, query, time.Now())
	if err != nil {
		return math.NaN(), err
	}
	vector, ok := v.(model.Vector)
	if !ok {
		return math.NaN(), errors.New("expect vector type")
	}
	if vector.Len() < 1 {
		return math.NaN(), nil
	}
	return float64(vector[0].Value), nil
}

func avgTiDBClusterMetricByInstance(ctx context.Context, api promcli.API, query string, ran promcli.Range) ([]MetricValue, error) {
	v, _, err := api.QueryRange(ctx, query, ran)
	if err != nil {
		return nil, err
	}

	matrix, ok := v.(model.Matrix)
	if !ok {
		return nil, errors.New("expect matrix type")
	}

	results := make([]MetricValue, 0)

	for _, ss := range matrix {
		var (
			total float64
			count int
		)

		for _, v := range ss.Values {
			total += float64(v.Value)
			count++
		}

		va := total / float64(count)
		if component, ok := ss.Metric[model.LabelName("component")]; ok {
			results = append(results, MetricValue{
				Component: string(component),
				Value:     uaDouble(va),
			})
		}
	}

	return results, nil
}

func GetMinValue(values []MetricValue) MetricValue {
	if len(values) == 0 {
		return MetricValue{
			Value: uaDouble(math.NaN()),
		}
	}

	sort.Slice(values, func(i, j int) bool {
		return values[i].Value < values[j].Value
	})

	return values[0]
}

func GetMaxValue(values []MetricValue) MetricValue {
	if len(values) == 0 {
		return MetricValue{
			Value: uaDouble(math.NaN()),
		}
	}

	sort.Slice(values, func(i, j int) bool {
		return values[i].Value > values[j].Value
	})

	return values[0]
}

func Division(numerator float64, denominator int64) uaDouble {
	if math.IsNaN(numerator) || denominator == 0 {
		return uaDouble(math.NaN())
	}

	v, err := strconv.ParseFloat(fmt.Sprintf("%.3f", numerator/float64(denominator)), 64)
	if err != nil {
		log.Error("division failed", zap.Float64("numerator", numerator), zap.Int64("denominator", denominator))
		return uaDouble(math.NaN())
	}
	return uaDouble(v)
}

type uaDouble float64

func (v uaDouble) String() string {
	data, err := v.MarshalJSON()
	if err != nil {
		return "NaN"
	}

	return strings.Trim(string(data), `"`)
}

func (v uaDouble) MarshalJSON() ([]byte, error) {
	var s string
	switch {
	case math.IsInf(float64(v), 1):
		s = "+Infinite"
	case math.IsInf(float64(v), -1):
		s = "-Infinite"
	case math.IsNaN(float64(v)):
		s = "NaN"
	default:
		s = strconv.FormatFloat(float64(v), 'f', 3, 64)
	}
	return []byte(`"` + s + `"`), nil
}

func (v *uaDouble) UnmarshalJSON(b []byte) error {
	switch {
	case bytes.Equal(b, []byte(`"+Infinite"`)):
		*v = uaDouble(math.Inf(1))
	case bytes.Equal(b, []byte(`"-Infinite"`)):
		*v = uaDouble(math.Inf(-1))
	case bytes.Equal(b, []byte(`"Nan"`)):
		*v = uaDouble(math.NaN())
	case bytes.Equal(b, []byte(`"NaN"`)):
		*v = uaDouble(math.NaN())
	default:
		n, err := strconv.ParseFloat(string(b), 64)
		if err != nil {
			return err
		}
		*v = uaDouble(n)
	}
	return nil
}

func IsExecutionPlanRedact(version string) bool {
	version = strings.TrimPrefix(version, "v")
	versionNumber := strings.Split(version, ".")
	if len(versionNumber) < 3 {
		return false
	}
	major, err := strconv.Atoi(versionNumber[0])
	if err != nil {
		return false
	}
	minor, err := strconv.Atoi(versionNumber[1])
	if err != nil {
		return false
	}
	patchVersion, _, _ := strings.Cut(versionNumber[2], "-")
	patchVersion, _, _ = strings.Cut(patchVersion, "+")
	patch, err := strconv.Atoi(patchVersion)
	if err != nil {
		return false
	}
	if major < 7 {
		return false
	}
	// 7.5.4+
	if major == 7 {
		if minor == 5 && patch >= 4 {
			return true
		}
		return false
	}
	if major == 8 {
		// 8.1.1+
		if minor == 1 && patch >= 1 {
			return true
		}
		// 8.3+
		if minor >= 3 {
			return true
		}
		return false
	}
	return true
}
