package provider

import (
	"errors"
	"fmt"
	"github.com/pingcap/tep/pkg/apis/apiserver"
	"github.com/pingcap/tep/pkg/metrics/client"

	"github.com/prometheus/common/model"
)

type metricConverter struct{}

// NewMetricConverter creates a MetricCoverter, capable of converting any of the three metric types
// returned by the Prometheus client into external metrics types.
func NewMetricConverter() MetricConverter {
	return &metricConverter{}
}

func (c *metricConverter) Convert(info MetricInfo, queryResult client.QueryResult) (*apiserver.MetricValueList, error) {
	if queryResult.Type == model.ValScalar {
		return c.convertScalar(info, queryResult)
	}

	if queryResult.Type == model.ValVector {
		return c.convertVector(info, queryResult)
	}

	if queryResult.Type == model.ValMatrix {
		return c.convertMatrix(info, queryResult)
	}

	return nil, errors.New("encountered an unexpected query result type")
}

func (c *metricConverter) convertSample(info MetricInfo, sample *model.Sample) (*apiserver.MetricValue, error) {
	labels := c.convertLabels(sample.Metric, info)

	p := apiserver.SamplePair{
		Timestamp: apiserver.Time(int64(sample.Timestamp)),
		Value:     apiserver.SampleValue(float64(sample.Value)),
	}

	singleMetric := apiserver.MetricValue{
		Values:       []apiserver.SamplePair{p},
		MetricLabels: labels,
	}

	return &singleMetric, nil
}

func (c *metricConverter) convertSampleStream(info MetricInfo, sample *model.SampleStream) (*apiserver.MetricValue, error) {
	labels := c.convertLabels(sample.Metric, info)

	singleMetric := apiserver.MetricValue{
		Values:       convert2InnerValue(sample.Values),
		MetricLabels: labels,
	}

	return &singleMetric, nil
}

func convert2InnerValue(ps []model.SamplePair) []apiserver.SamplePair {
	items := []apiserver.SamplePair{}
	for _, sample := range ps {
		p := apiserver.SamplePair{
			Timestamp: apiserver.Time(int64(sample.Timestamp)),
			Value:     apiserver.SampleValue(float64(sample.Value)),
		}
		items = append(items, p)
	}
	return items
}

func (c *metricConverter) convertLabels(inLabels model.Metric, info MetricInfo) map[string]string {
	numLabels := len(inLabels)
	outLabels := make(map[string]string, numLabels)
	for labelName, labelVal := range inLabels {
		outLabels[string(labelName)] = string(labelVal)
	}

	if info.AppendLabels != nil {
		for labelName, labelVal := range info.AppendLabels {
			if _, ok := outLabels[labelName]; !ok {
				outLabels[labelName] = labelVal
			}
		}
	}

	return outLabels
}

func (c *metricConverter) convertVector(info MetricInfo, queryResult client.QueryResult) (*apiserver.MetricValueList, error) {
	if queryResult.Type != model.ValVector {
		return nil, errors.New("incorrect query result type")
	}

	toConvert := *queryResult.Vector

	if toConvert == nil {
		return nil, errors.New("the provided input did not contain vector query results")
	}

	items := []apiserver.MetricValue{}
	metricValueList := apiserver.MetricValueList{
		Items: items,
	}

	numSamples := toConvert.Len()
	if numSamples == 0 {
		return &metricValueList, nil
	}

	for _, val := range toConvert {
		singleMetric, err := c.convertSample(info, val)

		if err != nil {
			return nil, fmt.Errorf("unable to convert vector: %v", err)
		}

		items = append(items, *singleMetric)
	}

	metricValueList = apiserver.MetricValueList{
		Items: items,
	}
	return &metricValueList, nil
}

func (c *metricConverter) convertScalar(info MetricInfo, queryResult client.QueryResult) (*apiserver.MetricValueList, error) {
	if queryResult.Type != model.ValScalar {
		return nil, errors.New("scalarConverter can only convert scalar query results")
	}

	toConvert := queryResult.Scalar

	if toConvert == nil {
		return nil, errors.New("the provided input did not contain scalar query results")
	}

	p := apiserver.SamplePair{
		Timestamp: apiserver.Time(int64(toConvert.Timestamp)),
		Value:     apiserver.SampleValue(float64(toConvert.Value)),
	}

	result := apiserver.MetricValueList{
		Items: []apiserver.MetricValue{
			{
				Values: []apiserver.SamplePair{p},
			},
		},
	}
	return &result, nil
}

func (c *metricConverter) convertMatrix(info MetricInfo, queryResult client.QueryResult) (*apiserver.MetricValueList, error) {
	if queryResult.Type != model.ValMatrix {
		return nil, errors.New("incorrect query result type")
	}

	toConvert := *queryResult.Matrix

	if toConvert == nil {
		return nil, errors.New("the provided input did not contain vector query results")
	}

	items := []apiserver.MetricValue{}
	metricValueList := apiserver.MetricValueList{
		Items: items,
	}

	numSamples := toConvert.Len()
	if numSamples == 0 {
		return &metricValueList, nil
	}

	for _, val := range toConvert {
		singleMetric, err := c.convertSampleStream(info, val)

		if err != nil {
			return nil, fmt.Errorf("unable to convert vector: %v", err)
		}

		items = append(items, *singleMetric)
	}

	metricValueList = apiserver.MetricValueList{
		Items: items,
	}
	return &metricValueList, nil
}
