package cloudwatch

import (
	"fmt"
	"sort"
	"strconv"
	"strings"
	"time"

	"github.com/aws/aws-sdk-go/service/cloudwatch"
	"github.com/grafana/grafana-plugin-sdk-go/data"
)

func (e *cloudWatchExecutor) parseResponse(metricDataOutputs []*cloudwatch.GetMetricDataOutput,
	queries map[string]*cloudWatchQuery) ([]*cloudwatchResponse, error) {
	// Map from result ID -> label -> result
	mdrs := make(map[string]map[string]*cloudwatch.MetricDataResult)
	labels := map[string][]string{}
	for _, mdo := range metricDataOutputs {
		requestExceededMaxLimit := false
		for _, message := range mdo.Messages {
			if *message.Code == "MaxMetricsExceeded" {
				requestExceededMaxLimit = true
			}
		}

		for _, r := range mdo.MetricDataResults {
			id := *r.Id
			label := *r.Label
			if _, exists := mdrs[id]; !exists {
				mdrs[id] = make(map[string]*cloudwatch.MetricDataResult)
				mdrs[id][label] = r
				labels[id] = append(labels[id], label)
			} else if _, exists := mdrs[id][label]; !exists {
				mdrs[id][label] = r
				labels[id] = append(labels[id], label)
			} else {
				mdr := mdrs[id][label]
				mdr.Timestamps = append(mdr.Timestamps, r.Timestamps...)
				mdr.Values = append(mdr.Values, r.Values...)
				if *r.StatusCode == "Complete" {
					mdr.StatusCode = r.StatusCode
				}
			}
			queries[id].RequestExceededMaxLimit = requestExceededMaxLimit
		}
	}

	cloudWatchResponses := make([]*cloudwatchResponse, 0, len(mdrs))
	for id, lr := range mdrs {
		query := queries[id]
		frames, partialData, err := parseMetricResults(lr, labels[id], query)
		if err != nil {
			return nil, err
		}

		response := &cloudwatchResponse{
			DataFrames:              frames,
			Period:                  query.Period,
			Expression:              query.UsedExpression,
			RefId:                   query.RefId,
			Id:                      query.Id,
			RequestExceededMaxLimit: query.RequestExceededMaxLimit,
			PartialData:             partialData,
		}
		cloudWatchResponses = append(cloudWatchResponses, response)
	}

	return cloudWatchResponses, nil
}

func parseMetricResults(results map[string]*cloudwatch.MetricDataResult, labels []string,
	query *cloudWatchQuery) (data.Frames, bool, error) {
	partialData := false
	frames := data.Frames{}
	for _, label := range labels {
		result := results[label]
		if *result.StatusCode != "Complete" {
			partialData = true
		}

		for _, message := range result.Messages {
			if *message.Code == "ArithmeticError" {
				return nil, false, fmt.Errorf("ArithmeticError in query %q: %s", query.RefId, *message.Value)
			}
		}

		// In case a multi-valued dimension is used and the cloudwatch query yields no values, create one empty time
		// series for each dimension value. Use that dimension value to expand the alias field
		if len(result.Values) == 0 && query.isMultiValuedDimensionExpression() {
			series := 0
			multiValuedDimension := ""
			for key, values := range query.Dimensions {
				if len(values) > series {
					series = len(values)
					multiValuedDimension = key
				}
			}

			for _, value := range query.Dimensions[multiValuedDimension] {
				tags := map[string]string{multiValuedDimension: value}
				for key, values := range query.Dimensions {
					if key != multiValuedDimension && len(values) > 0 {
						tags[key] = values[0]
					}
				}

				timeField := data.NewField(data.TimeSeriesTimeFieldName, nil, []*time.Time{})
				valueField := data.NewField(data.TimeSeriesValueFieldName, tags, []*float64{})

				frameName := formatAlias(query, query.Stats, tags, label)
				valueField.SetConfig(&data.FieldConfig{DisplayNameFromDS: frameName})

				emptyFrame := data.Frame{
					Name: frameName,
					Fields: []*data.Field{
						timeField,
						valueField,
					},
					RefID: query.RefId,
				}
				frames = append(frames, &emptyFrame)
			}
		} else {
			dims := make([]string, 0, len(query.Dimensions))
			for k := range query.Dimensions {
				dims = append(dims, k)
			}
			sort.Strings(dims)

			tags := data.Labels{}
			for _, dim := range dims {
				values := query.Dimensions[dim]
				if len(values) == 1 && values[0] != "*" {
					tags[dim] = values[0]
				} else {
					for _, value := range values {
						if value == label || value == "*" {
							tags[dim] = label
						} else if strings.Contains(label, value) {
							tags[dim] = value
						}
					}
				}
			}

			timestamps := []*time.Time{}
			points := []*float64{}
			for j, t := range result.Timestamps {
				if j > 0 {
					expectedTimestamp := result.Timestamps[j-1].Add(time.Duration(query.Period) * time.Second)
					if expectedTimestamp.Before(*t) {
						timestamps = append(timestamps, &expectedTimestamp)
						points = append(points, nil)
					}
				}
				val := result.Values[j]
				timestamps = append(timestamps, t)
				points = append(points, val)
			}

			timeField := data.NewField(data.TimeSeriesTimeFieldName, nil, timestamps)
			valueField := data.NewField(data.TimeSeriesValueFieldName, tags, points)

			frameName := formatAlias(query, query.Stats, tags, label)
			valueField.SetConfig(&data.FieldConfig{DisplayNameFromDS: frameName})

			frame := data.Frame{
				Name: frameName,
				Fields: []*data.Field{
					timeField,
					valueField,
				},
				RefID: query.RefId,
			}
			frames = append(frames, &frame)
		}
	}

	return frames, partialData, nil
}

func formatAlias(query *cloudWatchQuery, stat string, dimensions map[string]string, label string) string {
	region := query.Region
	namespace := query.Namespace
	metricName := query.MetricName
	period := strconv.Itoa(query.Period)

	if query.isUserDefinedSearchExpression() {
		pIndex := strings.LastIndex(query.Expression, ",")
		period = strings.Trim(query.Expression[pIndex+1:], " )")
		sIndex := strings.LastIndex(query.Expression[:pIndex], ",")
		stat = strings.Trim(query.Expression[sIndex+1:pIndex], " '")
	}

	if len(query.Alias) == 0 && query.isMathExpression() {
		return query.Id
	}
	if len(query.Alias) == 0 && query.isInferredSearchExpression() && !query.isMultiValuedDimensionExpression() {
		return label
	}

	data := map[string]string{
		"region":    region,
		"namespace": namespace,
		"metric":    metricName,
		"stat":      stat,
		"period":    period,
	}
	if len(label) != 0 {
		data["label"] = label
	}
	for k, v := range dimensions {
		data[k] = v
	}

	result := aliasFormat.ReplaceAllFunc([]byte(query.Alias), func(in []byte) []byte {
		labelName := strings.Replace(string(in), "{{", "", 1)
		labelName = strings.Replace(labelName, "}}", "", 1)
		labelName = strings.TrimSpace(labelName)
		if val, exists := data[labelName]; exists {
			return []byte(val)
		}

		return in
	})

	if string(result) == "" {
		return metricName + "_" + stat
	}

	return string(result)
}
