package expr

import (
	"fmt"
	"sort"
	"time"

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

const (
	SQLMetricFieldName  = "__metric_name__"
	SQLValueFieldName   = "__value__"
	SQLDisplayFieldName = "__display_name__"

	// These are not types in the SDK or dataplane contract yet.
	numericFullLongType    = "numeric-full-long"
	timeseriesFullLongType = "timeseries-full-long"
)

func ConvertToFullLong(frames data.Frames) (data.Frames, error) {
	if len(frames) == 0 {
		return frames, nil
	}

	var inputType data.FrameType
	if frames[0].Meta != nil && frames[0].Meta.Type != "" {
		inputType = frames[0].Meta.Type
	} else {
		// shouldn't hit this when calling from handleSqlInput as supportedToLongConversion is called first
		return nil, fmt.Errorf("input frame missing FrameMeta.Type")
	}

	switch inputType {
	case data.FrameTypeNumericMulti:
		return convertNumericMultiToFullLong(frames)
	case data.FrameTypeNumericWide:
		return convertNumericWideToFullLong(frames)
	case data.FrameTypeTimeSeriesMulti:
		return convertTimeSeriesMultiToFullLong(frames)
	case data.FrameTypeTimeSeriesWide:
		return convertTimeSeriesWideToFullLong(frames)
	default:
		// Shouldn't hit this when calling from handleSqlInput as supportedToLongConversion is called first
		return nil, fmt.Errorf("unsupported input type %s for full long conversion", inputType)
	}
}

func convertNumericMultiToFullLong(frames data.Frames) (data.Frames, error) {
	wide := convertNumericMultiToNumericWide(frames)
	return convertNumericWideToFullLong(wide)
}

func convertNumericWideToFullLong(frames data.Frames) (data.Frames, error) {
	if len(frames) != 1 {
		return nil, fmt.Errorf("expected exactly one frame for wide format, but got %d", len(frames))
	}
	inputFrame := frames[0]
	if inputFrame.Rows() > 1 {
		return nil, fmt.Errorf("expected no more than one row in the frame, but got %d", inputFrame.Rows())
	}

	var (
		metricCol     = make([]string, 0, len(inputFrame.Fields))
		valueCol      = make([]*float64, 0, len(inputFrame.Fields))
		displayCol    = make([]*string, 0, len(inputFrame.Fields))
		hasDisplayCol bool
	)

	labelKeySet := map[string]struct{}{}
	for _, field := range inputFrame.Fields {
		if !field.Type().Numeric() {
			continue
		}
		val, err := field.FloatAt(0)
		if err != nil {
			continue
		}
		v := val
		valueCol = append(valueCol, &v)
		metricCol = append(metricCol, field.Name)

		// Display name
		var d *string
		if field.Config != nil && field.Config.DisplayNameFromDS != "" {
			s := field.Config.DisplayNameFromDS
			d = &s
			hasDisplayCol = true
		}
		displayCol = append(displayCol, d)

		for k := range field.Labels {
			labelKeySet[k] = struct{}{}
		}
	}

	labelKeys := make([]string, 0, len(labelKeySet))

	labelValues := make(map[string][]*string)
	for k := range labelKeySet {
		labelKeys = append(labelKeys, k)
		labelValues[k] = make([]*string, 0, len(valueCol))
	}
	sort.Strings(labelKeys)

	for _, field := range inputFrame.Fields {
		if !field.Type().Numeric() {
			continue
		}
		for _, k := range labelKeys {
			var val *string
			if field.Labels != nil {
				if v, ok := field.Labels[k]; ok {
					val = &v
				}
			}
			labelValues[k] = append(labelValues[k], val)
		}
	}

	fields := []*data.Field{
		data.NewField(SQLMetricFieldName, nil, metricCol),
		data.NewField(SQLValueFieldName, nil, valueCol),
	}
	if hasDisplayCol {
		fields = append(fields, data.NewField(SQLDisplayFieldName, nil, displayCol))
	}
	for _, k := range labelKeys {
		fields = append(fields, data.NewField(k, nil, labelValues[k]))
	}

	out := data.NewFrame("", fields...)
	out.Meta = &data.FrameMeta{Type: numericFullLongType}
	return data.Frames{out}, nil
}

func convertTimeSeriesMultiToFullLong(frames data.Frames) (data.Frames, error) {
	type row struct {
		t       time.Time
		value   *float64
		metric  string
		display *string
		labels  data.Labels
	}

	var rows []row
	labelKeysSet := map[string]struct{}{}
	hasDisplayCol := false

	for _, frame := range frames {
		var timeField *data.Field
		for _, f := range frame.Fields {
			if f.Type() == data.FieldTypeTime {
				timeField = f
				break
			}
		}
		if timeField == nil {
			return nil, fmt.Errorf("missing time field")
		}
		for _, f := range frame.Fields {
			if !f.Type().Numeric() {
				continue
			}
			var display *string
			if f.Config != nil && f.Config.DisplayNameFromDS != "" {
				s := f.Config.DisplayNameFromDS
				display = &s
				hasDisplayCol = true
			}
			for i := 0; i < f.Len(); i++ {
				t := timeField.At(i).(time.Time)
				v, err := f.FloatAt(i)
				if err != nil {
					continue
				}
				val := v
				rows = append(rows, row{
					t:       t,
					value:   &val,
					metric:  f.Name,
					display: display,
					labels:  f.Labels,
				})
				for k := range f.Labels {
					labelKeysSet[k] = struct{}{}
				}
			}
		}
	}

	labelKeys := make([]string, 0, len(labelKeysSet))
	for k := range labelKeysSet {
		labelKeys = append(labelKeys, k)
	}
	sort.Strings(labelKeys)
	sort.SliceStable(rows, func(i, j int) bool {
		if rows[i].t.Equal(rows[j].t) {
			return rows[i].metric < rows[j].metric
		}
		return rows[i].t.Before(rows[j].t)
	})

	times := make([]time.Time, len(rows))
	values := make([]*float64, len(rows))
	metrics := make([]string, len(rows))
	var displays []*string
	if hasDisplayCol {
		displays = make([]*string, len(rows))
	}
	labels := make(map[string][]*string)
	for _, k := range labelKeys {
		labels[k] = make([]*string, len(rows))
	}

	for i, r := range rows {
		times[i] = r.t
		values[i] = r.value
		metrics[i] = r.metric
		if hasDisplayCol {
			displays[i] = r.display
		}
		for _, k := range labelKeys {
			if v, ok := r.labels[k]; ok {
				labels[k][i] = &v
			}
		}
	}

	fields := []*data.Field{
		data.NewField("time", nil, times),
		data.NewField(SQLValueFieldName, nil, values),
		data.NewField(SQLMetricFieldName, nil, metrics),
	}
	if hasDisplayCol {
		fields = append(fields, data.NewField(SQLDisplayFieldName, nil, displays))
	}
	for _, k := range labelKeys {
		fields = append(fields, data.NewField(k, nil, labels[k]))
	}

	out := data.NewFrame("", fields...)
	out.Meta = &data.FrameMeta{Type: timeseriesFullLongType}
	return data.Frames{out}, nil
}

func convertTimeSeriesWideToFullLong(frames data.Frames) (data.Frames, error) {
	if len(frames) != 1 {
		return nil, fmt.Errorf("expected exactly one frame for wide format, but got %d", len(frames))
	}
	frame := frames[0]

	var timeField *data.Field
	for _, f := range frame.Fields {
		if f.Type() == data.FieldTypeTime {
			timeField = f
			break
		}
	}
	if timeField == nil {
		return nil, fmt.Errorf("time field not found in TimeSeriesWide frame")
	}

	type row struct {
		t       time.Time
		value   *float64
		metric  string
		display *string
		labels  data.Labels
	}

	var (
		rows          []row
		labelKeysSet  = map[string]struct{}{}
		hasDisplayCol bool
	)

	// Collect all label keys
	for _, f := range frame.Fields {
		if !f.Type().Numeric() {
			continue
		}
		for k := range f.Labels {
			labelKeysSet[k] = struct{}{}
		}
	}

	labelKeys := make([]string, 0, len(labelKeysSet))
	for k := range labelKeysSet {
		labelKeys = append(labelKeys, k)
	}
	sort.Strings(labelKeys)

	timeLen := timeField.Len()
	for _, f := range frame.Fields {
		if !f.Type().Numeric() {
			continue
		}
		var display *string
		if f.Config != nil && f.Config.DisplayNameFromDS != "" {
			s := f.Config.DisplayNameFromDS
			display = &s
			hasDisplayCol = true
		}
		for i := 0; i < timeLen; i++ {
			t := timeField.At(i).(time.Time)
			v, err := f.FloatAt(i)
			if err != nil {
				continue
			}
			val := v
			rows = append(rows, row{
				t:       t,
				value:   &val,
				metric:  f.Name,
				display: display,
				labels:  f.Labels,
			})
		}
	}

	sort.SliceStable(rows, func(i, j int) bool {
		if rows[i].t.Equal(rows[j].t) {
			return rows[i].metric < rows[j].metric
		}
		return rows[i].t.Before(rows[j].t)
	})

	times := make([]time.Time, len(rows))
	values := make([]*float64, len(rows))
	metrics := make([]string, len(rows))
	var displays []*string
	if hasDisplayCol {
		displays = make([]*string, len(rows))
	}
	labels := make(map[string][]*string)
	for _, k := range labelKeys {
		labels[k] = make([]*string, len(rows))
	}

	for i, r := range rows {
		times[i] = r.t
		values[i] = r.value
		metrics[i] = r.metric
		if hasDisplayCol {
			displays[i] = r.display
		}
		for _, k := range labelKeys {
			if v, ok := r.labels[k]; ok {
				labels[k][i] = &v
			}
		}
	}

	fields := []*data.Field{
		data.NewField("time", nil, times),
		data.NewField(SQLValueFieldName, nil, values),
		data.NewField(SQLMetricFieldName, nil, metrics),
	}
	if hasDisplayCol {
		fields = append(fields, data.NewField(SQLDisplayFieldName, nil, displays))
	}
	for _, k := range labelKeys {
		fields = append(fields, data.NewField(k, nil, labels[k]))
	}

	out := data.NewFrame("", fields...)
	out.Meta = &data.FrameMeta{Type: timeseriesFullLongType}
	return data.Frames{out}, nil
}

func supportedToLongConversion(inputType data.FrameType) bool {
	switch inputType {
	case data.FrameTypeNumericMulti, data.FrameTypeNumericWide:
		return true
	case data.FrameTypeTimeSeriesMulti, data.FrameTypeTimeSeriesWide:
		return true
	default:
		return false
	}
}

func convertNumericMultiToNumericWide(frames data.Frames) data.Frames {
	if len(frames) == 0 {
		return nil
	}

	out := data.NewFrame("")
	for _, frame := range frames {
		for _, field := range frame.Fields {
			if field.Type().Numeric() {
				out.Fields = append(out.Fields, field)
			}
		}
	}
	out.Meta = &data.FrameMeta{Type: data.FrameTypeNumericWide}
	return data.Frames{out}
}
