package buffered

import (
	"errors"
	"fmt"
	"io"
	"strings"
	"time"

	"github.com/grafana/grafana-plugin-sdk-go/backend"
	"github.com/grafana/grafana-plugin-sdk-go/data"
	"github.com/influxdata/influxql"
	jsoniter "github.com/json-iterator/go"

	"github.com/grafana/grafana/pkg/tsdb/influxdb/influxql/util"
	"github.com/grafana/grafana/pkg/tsdb/influxdb/models"
)

func ResponseParse(buf io.ReadCloser, statusCode int, query *models.Query) *backend.DataResponse {
	return parse(buf, statusCode, query)
}

// parse is the same as Parse, but without the io.ReadCloser (we don't need to
// close the buffer)
func parse(buf io.Reader, statusCode int, query *models.Query) *backend.DataResponse {
	response, jsonErr := parseJSON(buf)

	if statusCode/100 != 2 {
		errorStr := response.Error
		if errorStr == "" {
			errorStr = response.Message
		}
		return &backend.DataResponse{
			Error:       fmt.Errorf("InfluxDB returned error: %s", errorStr),
			ErrorSource: backend.ErrorSourceFromHTTPStatus(statusCode),
		}
	}

	if jsonErr != nil {
		return &backend.DataResponse{Error: jsonErr}
	}

	if response.Error != "" {
		return &backend.DataResponse{
			Error:       errors.New(response.Error),
			ErrorSource: backend.ErrorSourceDownstream,
		}
	}

	result := response.Results[0]
	if result.Error != "" {
		return &backend.DataResponse{
			Error:       errors.New(result.Error),
			ErrorSource: backend.ErrorSourceDownstream,
		}
	}

	if query.ResultFormat == "table" {
		return &backend.DataResponse{Frames: transformRowsForTable(result.Series, *query)}
	}

	return &backend.DataResponse{Frames: transformRowsForTimeSeries(result.Series, *query)}
}

func parseJSON(buf io.Reader) (models.Response, error) {
	var response models.Response

	json := jsoniter.ConfigCompatibleWithStandardLibrary

	dec := json.NewDecoder(buf)

	err := dec.Decode(&response)

	return response, err
}

func transformRowsForTable(rows []models.Row, query models.Query) data.Frames {
	if len(rows) == 0 {
		return make([]*data.Frame, 0)
	}

	frames := make([]*data.Frame, 0, 1)

	newFrame := data.NewFrame(rows[0].Name)
	newFrame.Meta = &data.FrameMeta{
		ExecutedQueryString:    query.RawQuery,
		PreferredVisualization: util.GetVisType(query.ResultFormat),
	}

	conLen := len(rows[0].Columns)
	if rows[0].Columns[0] == "time" {
		newFrame.Fields = append(newFrame.Fields, newTimeField(rows))
	} else {
		newFrame.Fields = append(newFrame.Fields, newValueFields(rows, nil, 0, 1)...)
	}

	newFrame.Fields = append(newFrame.Fields, newTagField(rows, nil)...)
	newFrame.Fields = append(newFrame.Fields, newValueFields(rows, nil, 1, conLen)...)

	frames = append(frames, newFrame)
	return frames
}

func newTimeField(rows []models.Row) *data.Field {
	var timeArray []time.Time
	for _, row := range rows {
		for _, valuePair := range row.Values {
			timestamp, timestampErr := util.ParseTimestamp(valuePair[0])
			// we only add this row if the timestamp is valid
			if timestampErr != nil {
				continue
			}

			timeArray = append(timeArray, timestamp)
		}
	}

	timeField := data.NewField("Time", nil, timeArray)
	return timeField
}

func newTagField(rows []models.Row, labels data.Labels) []*data.Field {
	fields := make([]*data.Field, 0, len(rows[0].Tags))

	for key := range rows[0].Tags {
		tagField := data.NewField(key, labels, []*string{})
		for _, row := range rows {
			for range row.Values {
				value := row.Tags[key]
				tagField.Append(&value)
			}
		}
		tagField.SetConfig(&data.FieldConfig{DisplayNameFromDS: key})
		fields = append(fields, tagField)
	}

	return fields
}

func newValueFields(rows []models.Row, labels data.Labels, colIdxStart, colIdxEnd int) []*data.Field {
	fields := make([]*data.Field, 0)

	for colIdx := colIdxStart; colIdx < colIdxEnd; colIdx++ {
		var valueField *data.Field
		var floatArray []*float64
		var stringArray []*string
		var boolArray []*bool

		for _, row := range rows {
			valType := util.Typeof(row.Values, colIdx)

			for _, valuePair := range row.Values {
				switch valType {
				case "string":
					value, ok := valuePair[colIdx].(string)
					if ok {
						// we handle null values by adding nil to floatArray
						// if then we see the valueField should be a sting field
						// we append those nil elements into the stringArray
						// then we clear the floatArray
						// these steps are necessary for the responses like in string_column_with_null_value.json
						for range floatArray {
							stringArray = append(stringArray, nil)
						}
						floatArray = nil
						stringArray = append(stringArray, &value)
					} else {
						stringArray = append(stringArray, nil)
					}
				case "json.Number":
					value := util.ParseNumber(valuePair[colIdx])
					floatArray = append(floatArray, value)
				case "float64":
					if value, ok := valuePair[colIdx].(float64); ok {
						floatArray = append(floatArray, &value)
					} else {
						floatArray = append(floatArray, nil)
					}
				case "bool":
					value, ok := valuePair[colIdx].(bool)
					if ok {
						// we handle null values by adding nil to floatArray
						// if then we see the valueField should be a bool field
						// we append those nil elements into the boolArray
						// then we clear the floatArray
						for range floatArray {
							boolArray = append(boolArray, nil)
						}
						floatArray = nil
						boolArray = append(boolArray, &value)
					} else {
						boolArray = append(boolArray, nil)
					}
				case "null":
					// If there is already a valueField, instead of adding nil to floatArray
					// we add nil to the valueField and to the array of valueField constructed from
					if valueField != nil {
						valueFieldType := valueField.Type()
						switch valueFieldType {
						case data.FieldTypeNullableString:
							stringArray = append(stringArray, nil)
						case data.FieldTypeNullableBool:
							boolArray = append(boolArray, nil)
						default:
							floatArray = append(floatArray, nil)
						}
						valueField.Append(nil)
					} else {
						// If there is no valueField created before we add the nil value to floatArray
						// when we have the real value of the field these will be appended to the field
						floatArray = append(floatArray, nil)
					}
				}
			}

			switch valType {
			case "string":
				valueField = data.NewField(row.Columns[colIdx], labels, stringArray)
			case "json.Number":
				valueField = data.NewField(row.Columns[colIdx], labels, floatArray)
			case "float64":
				valueField = data.NewField(row.Columns[colIdx], labels, floatArray)
			case "bool":
				valueField = data.NewField(row.Columns[colIdx], labels, boolArray)
			case "null":
				if valueField == nil {
					valueField = data.NewField(row.Columns[colIdx], labels, floatArray)
				}
			}

			valueField.SetConfig(&data.FieldConfig{DisplayNameFromDS: row.Columns[colIdx]})
		}
		fields = append(fields, valueField)
	}

	return fields
}

func transformRowsForTimeSeries(rows []models.Row, query models.Query) data.Frames {
	// pre-allocate frames - this can save many allocations
	cols := 0
	for _, row := range rows {
		cols += len(row.Columns)
	}

	if len(rows) == 0 {
		return make([]*data.Frame, 0)
	}

	// Preallocate for the worst-case scenario
	frames := make([]*data.Frame, 0, len(rows)*len(rows[0].Columns))

	// frameName is pre-allocated. So we can reuse it, saving memory.
	// It's sized for a reasonably-large name, but will grow if needed.
	frameName := make([]byte, 0, 128)

	for _, row := range rows {
		var hasTimeCol = false

		for _, column := range row.Columns {
			if strings.ToLower(column) == "time" {
				hasTimeCol = true
			}
		}

		if !hasTimeCol {
			newFrame := newFrameWithoutTimeField(row, query)
			frames = append(frames, newFrame)
		} else {
			for colIndex, column := range row.Columns {
				if column == "time" {
					continue
				}
				newFrame := newFrameWithTimeField(row, column, colIndex, query, frameName)
				frames = append(frames, newFrame)
			}
		}
	}

	if len(frames) > 0 {
		frames[0].Meta = &data.FrameMeta{
			ExecutedQueryString:    query.RawQuery,
			PreferredVisualization: util.GetVisType(query.ResultFormat),
		}
	}

	return frames
}

func newFrameWithTimeField(row models.Row, column string, colIndex int, query models.Query, frameName []byte) *data.Frame {
	var floatArray []*float64
	var stringArray []*string
	var boolArray []*bool
	valType := util.Typeof(row.Values, colIndex)

	timeArray := make([]time.Time, 0, len(row.Values))
	for _, valuePair := range row.Values {
		timestamp, timestampErr := util.ParseTimestamp(valuePair[0])
		// we only add this row if the timestamp is valid
		if timestampErr != nil {
			continue
		}

		timeArray = append(timeArray, timestamp)
		switch valType {
		case "string":
			value, ok := valuePair[colIndex].(string)
			if ok {
				stringArray = append(stringArray, &value)
			} else {
				stringArray = append(stringArray, nil)
			}
		case "json.Number":
			value := util.ParseNumber(valuePair[colIndex])
			floatArray = append(floatArray, value)
		case "float64":
			if value, ok := valuePair[colIndex].(float64); ok {
				floatArray = append(floatArray, &value)
			} else {
				floatArray = append(floatArray, nil)
			}
		case "bool":
			value, ok := valuePair[colIndex].(bool)
			if ok {
				boolArray = append(boolArray, &value)
			} else {
				boolArray = append(boolArray, nil)
			}
		case "null":
			floatArray = append(floatArray, nil)
		}
	}

	timeField := data.NewField("Time", nil, timeArray)

	var valueField *data.Field

	switch valType {
	case "string":
		valueField = data.NewField("Value", row.Tags, stringArray)
	case "json.Number":
		valueField = data.NewField("Value", row.Tags, floatArray)
	case "float64":
		valueField = data.NewField("Value", row.Tags, floatArray)
	case "bool":
		valueField = data.NewField("Value", row.Tags, boolArray)
	case "null":
		valueField = data.NewField("Value", row.Tags, floatArray)
	}

	name := string(util.FormatFrameName(row.Name, column, row.Tags, query, frameName[:]))
	valueField.SetConfig(&data.FieldConfig{DisplayNameFromDS: name})
	return data.NewFrame(name, timeField, valueField)
}

func newFrameWithoutTimeField(row models.Row, query models.Query) *data.Frame {
	var values []*string

	switch query.Statement.(type) {
	case *influxql.ShowMeasurementCardinalityStatement,
		*influxql.ShowSeriesCardinalityStatement,
		*influxql.ShowFieldKeyCardinalityStatement,
		*influxql.ShowTagValuesCardinalityStatement,
		*influxql.ShowTagKeyCardinalityStatement:
		// Handle all CARDINALITY queries
		for _, valuePair := range row.Values {
			if len(valuePair) >= 1 {
				values = append(values, util.ParseString(valuePair[0]))
			}
		}
	case *influxql.ShowDiagnosticsStatement:
		// Handle SHOW DIAGNOSTICS
		// https://docs.influxdata.com/platform/monitoring/influxdata-platform/tools/show-diagnostics/
		for _, valuePair := range row.Values {
			for _, vp := range valuePair {
				values = append(values, util.ParseString(vp))
			}
		}
	case *influxql.ShowTagValuesStatement:
		// Handle SHOW TAG VALUES (non-CARDINALITY)
		for _, valuePair := range row.Values {
			if len(valuePair) >= 2 {
				values = append(values, util.ParseString(valuePair[1]))
			}
		}
	default:
		// Handle other queries
		for _, valuePair := range row.Values {
			if len(valuePair) >= 1 {
				values = append(values, util.ParseString(valuePair[0]))
			}
		}
	}

	field := data.NewField("Value", nil, values)
	frame := data.NewFrame(row.Name, field)
	return frame
}
