package azuremonitor

import (
	"encoding/json"
	"fmt"
	"time"

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

// InsightsMetricsResultToFrame converts a MetricsResult (an Application Insights metrics query response) to a dataframe.
// Due to the dynamic nature of the MetricsResult object, the name of the metric, aggregation,
// and requested dimensions are used to determine the expected shape of the object.
// This builds all series into a single data.Frame with one time index (a wide formatted time series frame).
func InsightsMetricsResultToFrame(mr MetricsResult, metric, agg string, dimensions []string) (*data.Frame, error) {
	dimLen := len(dimensions)

	// The Response has both Start and End times, so we name the column "StartTime".
	frame := data.NewFrame("", data.NewField("StartTime", nil, []time.Time{}))

	fieldIdxMap := map[string]int{} // a map of a string representation of the labels to the Field index in the frame.

	rowCounter := 0 // row in the resulting frame

	if mr.Value == nil { // never seen this response, but to ensure there is no panic
		return nil, fmt.Errorf("unexpected nil response or response value in metrics result")
	}

	for _, seg := range *mr.Value.Segments { // each top level segment in the response shares timestamps.
		frame.Extend(1)
		frame.Set(0, rowCounter, seg.Start) // field 0 is the time field
		labels := data.Labels{}

		// handleLeafSegment is for the leaf MetricsSegmentInfo nodes in the response.
		// A leaf node contains an aggregated value, and when there are multiple dimensions, a label key/value pair.
		handleLeafSegment := func(s MetricsSegmentInfo) error {
			// since this is a dynamic response, everything we are interested in here from JSON
			// is Marshalled (mapped) into the AdditionalProperties property.
			v, err := valFromLeafAP(s.AdditionalProperties, metric, agg)
			if err != nil {
				return err
			}

			if dimLen != 0 { // when there are dimensions, the final dimension is in this inner segment.
				dimension := dimensions[dimLen-1]
				dimVal, err := dimValueFromAP(s.AdditionalProperties, dimension)
				if err != nil {
					return err
				}
				labels[dimension] = dimVal
			}

			if _, ok := fieldIdxMap[labels.String()]; !ok {
				// When we find a new combination of labels for the metric, a new Field is appended.
				frame.Fields = append(frame.Fields, data.NewField(metric, labels.Copy(), make([]*float64, rowCounter+1)))
				fieldIdxMap[labels.String()] = len(frame.Fields) - 1
			}

			frame.Set(fieldIdxMap[labels.String()], rowCounter, v)

			return nil
		}

		// Simple case with no segments/dimensions
		if dimLen == 0 {
			if err := handleLeafSegment(seg); err != nil {
				return nil, err
			}
			rowCounter++
			continue
		}

		// Multiple dimension case
		var traverse func(segments *[]MetricsSegmentInfo, depth int) error

		// traverse walks segments collecting dimensions into labels until leaf segments are
		// reached, and then handleInnerSegment is called. The final k/v label pair is
		// in the leaf segment.
		// A non-recursive implementation would probably be better.
		traverse = func(segments *[]MetricsSegmentInfo, depth int) error {
			if segments == nil {
				return nil
			}
			for _, seg := range *segments {
				if seg.Segments == nil {
					if err := handleLeafSegment(seg); err != nil {
						return err
					}
					continue
				}
				dimension := dimensions[depth]
				dimVal, err := dimValueFromAP(seg.AdditionalProperties, dimension)
				if err != nil {
					return err
				}
				labels[dimension] = dimVal
				if err := traverse(seg.Segments, depth+1); err != nil {
					return err
				}
			}
			return nil
		}

		if err := traverse(seg.Segments, 0); err != nil {
			return nil, err
		}
		rowCounter++
	}

	if len(frame.Fields) == 1 { // No data, only a time column, no sort
		return frame, nil
	}

	if err := data.SortWideFrameFields(frame, dimensions...); err != nil {
		return nil, err
	}

	return frame, nil
}

// valFromLeafAP extracts value for the given metric and aggregation (agg)
// from the dynamic AdditionalProperties properties of a leaf node. It is for use in the InsightsMetricsResultToFrame
// function.
func valFromLeafAP(ap map[string]interface{}, metric, agg string) (*float64, error) {
	if ap == nil {
		return nil, fmt.Errorf("expected additional properties for metric %v not found in leaf segment", metric)
	}
	met, ok := ap[metric]
	if !ok {
		return nil, fmt.Errorf("expected additional properties for metric %v not found in leaf segment", metric)
	}

	metMap, ok := met.(map[string]interface{})
	if !ok {
		return nil, fmt.Errorf("unexpected type for additional properties not found in leaf segment, want map[string]interface{}, but got %T", met)
	}
	metVal, ok := metMap[agg]
	if !ok {
		return nil, fmt.Errorf("expected value for aggregation %v not found in leaf segment", agg)
	}
	var v *float64
	if val, ok := metVal.(float64); ok {
		v = &val
	}

	return v, nil
}

// dimValueFromAP fetches the value as a string for the corresponding dimension from the dynamic AdditionalProperties properties of a leaf node. It is for use in the InsightsMetricsResultToFrame
// function.
func dimValueFromAP(ap map[string]interface{}, dimension string) (string, error) {
	rawDimValue, ok := ap[dimension]
	if !ok {
		return "", fmt.Errorf("expected dimension key %v not found in response", dimension)
	}
	dimValue, ok := rawDimValue.(string)
	if !ok {
		return "", fmt.Errorf("unexpected non-string value for the value for dimension %v, got type %T with a value of %v", dimension, rawDimValue, dimValue)
	}
	return dimValue, nil
}

// MetricsResult a metric result.
// This is copied from azure-sdk-for-go/services/preview/appinsights/v1/insights.
type MetricsResult struct {
	Value *MetricsResultInfo `json:"value,omitempty"`
}

// MetricsResultInfo a metric result data.
// This is copied from azure-sdk-for-go/services/preview/appinsights/v1/insights (except time Type is changed).
type MetricsResultInfo struct {
	// AdditionalProperties - Unmatched properties from the message are deserialized this collection
	AdditionalProperties map[string]interface{} `json:""`
	// Start - Start time of the metric.
	Start time.Time `json:"start,omitempty"`
	// End - Start time of the metric.
	End time.Time `json:"end,omitempty"`
	// Interval - The interval used to segment the metric data.
	Interval *string `json:"interval,omitempty"`
	// Segments - Segmented metric data (if segmented).
	Segments *[]MetricsSegmentInfo `json:"segments,omitempty"`
}

// MetricsSegmentInfo is a metric segment.
// This is copied from azure-sdk-for-go/services/preview/appinsights/v1/insights (except time Type is changed).
type MetricsSegmentInfo struct {
	// AdditionalProperties - Unmatched properties from the message are deserialized this collection
	AdditionalProperties map[string]interface{} `json:""`
	// Start - Start time of the metric segment (only when an interval was specified).
	Start time.Time `json:"start,omitempty"`
	// End - Start time of the metric segment (only when an interval was specified).
	End time.Time `json:"end,omitempty"`
	// Segments - Segmented metric data (if further segmented).
	Segments *[]MetricsSegmentInfo `json:"segments,omitempty"`
}

// UnmarshalJSON is the custom unmarshaler for MetricsSegmentInfo struct.
// This is copied from azure-sdk-for-go/services/preview/appinsights/v1/insights (except time Type is changed).
func (mri *MetricsSegmentInfo) UnmarshalJSON(body []byte) error {
	var m map[string]*json.RawMessage
	err := json.Unmarshal(body, &m)
	if err != nil {
		return err
	}
	for k, v := range m {
		switch k {
		default:
			if v != nil {
				var additionalProperties interface{}
				err = json.Unmarshal(*v, &additionalProperties)
				if err != nil {
					return err
				}
				if mri.AdditionalProperties == nil {
					mri.AdditionalProperties = make(map[string]interface{})
				}
				mri.AdditionalProperties[k] = additionalProperties
			}
		case "start":
			if v != nil {
				var start time.Time
				err = json.Unmarshal(*v, &start)
				if err != nil {
					return err
				}
				mri.Start = start
			}
		case "end":
			if v != nil {
				var end time.Time
				err = json.Unmarshal(*v, &end)
				if err != nil {
					return err
				}
				mri.End = end
			}
		case "segments":
			if v != nil {
				var segments []MetricsSegmentInfo
				err = json.Unmarshal(*v, &segments)
				if err != nil {
					return err
				}
				mri.Segments = &segments
			}
		}
	}

	return nil
}

// UnmarshalJSON is the custom unmarshaler for MetricsResultInfo struct.
// This is copied from azure-sdk-for-go/services/preview/appinsights/v1/insights (except time Type is changed).
func (mri *MetricsResultInfo) UnmarshalJSON(body []byte) error {
	var m map[string]*json.RawMessage
	err := json.Unmarshal(body, &m)
	if err != nil {
		return err
	}
	for k, v := range m {
		switch k {
		default:
			if v != nil {
				var additionalProperties interface{}
				err = json.Unmarshal(*v, &additionalProperties)
				if err != nil {
					return err
				}
				if mri.AdditionalProperties == nil {
					mri.AdditionalProperties = make(map[string]interface{})
				}
				mri.AdditionalProperties[k] = additionalProperties
			}
		case "start":
			if v != nil {
				var start time.Time
				err = json.Unmarshal(*v, &start)
				if err != nil {
					return err
				}
				mri.Start = start
			}
		case "end":
			if v != nil {
				var end time.Time
				err = json.Unmarshal(*v, &end)
				if err != nil {
					return err
				}
				mri.End = end
			}
		case "interval":
			if v != nil {
				var interval string
				err = json.Unmarshal(*v, &interval)
				if err != nil {
					return err
				}
				mri.Interval = &interval
			}
		case "segments":
			if v != nil {
				var segments []MetricsSegmentInfo
				err = json.Unmarshal(*v, &segments)
				if err != nil {
					return err
				}
				mri.Segments = &segments
			}
		}
	}

	return nil
}
