package testdatasource

import (
	"context"
	"encoding/csv"
	"errors"
	"fmt"
	"io"
	"os"
	"path/filepath"
	"regexp"
	"strconv"
	"strings"
	"time"

	"github.com/grafana/grafana-plugin-sdk-go/backend"
	"github.com/grafana/grafana-plugin-sdk-go/data"
	"github.com/grafana/grafana/pkg/components/simplejson"
)

func (p *testDataPlugin) handleCsvContentScenario(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
	resp := backend.NewQueryDataResponse()

	for _, q := range req.Queries {
		model, err := simplejson.NewJson(q.JSON)
		if err != nil {
			return nil, fmt.Errorf("failed to parse query json: %v", err)
		}

		csvContent := model.Get("csvContent").MustString()
		alias := model.Get("alias").MustString(q.RefID)

		frame, err := p.loadCsvContent(strings.NewReader(csvContent), alias)
		if err != nil {
			return nil, err
		}

		respD := resp.Responses[q.RefID]
		respD.Frames = append(respD.Frames, frame)
		resp.Responses[q.RefID] = respD
	}

	return resp, nil
}

func (p *testDataPlugin) handleCsvFileScenario(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
	resp := backend.NewQueryDataResponse()

	for _, q := range req.Queries {
		model, err := simplejson.NewJson(q.JSON)
		if err != nil {
			return nil, fmt.Errorf("failed to parse query json %v", err)
		}

		fileName := model.Get("csvFileName").MustString()

		if len(fileName) == 0 {
			continue
		}

		frame, err := p.loadCsvFile(fileName)

		if err != nil {
			return nil, err
		}

		respD := resp.Responses[q.RefID]
		respD.Frames = append(respD.Frames, frame)
		resp.Responses[q.RefID] = respD
	}

	return resp, nil
}

func (p *testDataPlugin) loadCsvFile(fileName string) (*data.Frame, error) {
	validFileName := regexp.MustCompile(`([\w_]+)\.csv`)

	if !validFileName.MatchString(fileName) {
		return nil, fmt.Errorf("invalid csv file name: %q", fileName)
	}

	filePath := filepath.Join(p.Cfg.StaticRootPath, "testdata", fileName)

	// Can ignore gosec G304 here, because we check the file pattern above
	// nolint:gosec
	fileReader, err := os.Open(filePath)
	if err != nil {
		return nil, fmt.Errorf("failed open file: %v", err)
	}

	defer func() {
		if err := fileReader.Close(); err != nil {
			p.logger.Warn("Failed to close file", "err", err, "path", fileName)
		}
	}()

	return p.loadCsvContent(fileReader, fileName)
}

func (p *testDataPlugin) loadCsvContent(ioReader io.Reader, name string) (*data.Frame, error) {
	reader := csv.NewReader(ioReader)

	// Read the header records
	headerFields, err := reader.Read()
	if err != nil {
		return nil, fmt.Errorf("failed to read header line: %v", err)
	}

	fields := []*data.Field{}
	fieldNames := []string{}
	fieldRawValues := [][]string{}

	for _, fieldName := range headerFields {
		fieldNames = append(fieldNames, strings.Trim(fieldName, " "))
		fieldRawValues = append(fieldRawValues, []string{})
	}

	for {
		lineValues, err := reader.Read()
		if errors.Is(err, io.EOF) {
			break // reached end of the file
		} else if err != nil {
			return nil, fmt.Errorf("failed to read line: %v", err)
		}

		for fieldIndex, value := range lineValues {
			fieldRawValues[fieldIndex] = append(fieldRawValues[fieldIndex], strings.Trim(value, " "))
		}
	}

	longest := 0
	for fieldIndex, rawValues := range fieldRawValues {
		fieldName := fieldNames[fieldIndex]
		field, err := csvValuesToField(rawValues)
		if err == nil {
			// Check if the values are actually a time field
			if strings.Contains(strings.ToLower(fieldName), "time") {
				timeField := toTimeField(field)
				if timeField != nil {
					field = timeField
				}
			}

			field.Name = fieldName
			fields = append(fields, field)
			if field.Len() > longest {
				longest = field.Len()
			}
		}
	}

	// Make all fields the same length
	for _, field := range fields {
		delta := field.Len() - longest
		if delta > 0 {
			field.Extend(delta)
		}
	}

	frame := data.NewFrame(name, fields...)
	return frame, nil
}

func csvLineToField(stringInput string) (*data.Field, error) {
	return csvValuesToField(strings.Split(strings.ReplaceAll(stringInput, " ", ""), ","))
}

func csvValuesToField(parts []string) (*data.Field, error) {
	if len(parts) < 1 {
		return nil, fmt.Errorf("csv must have at least one value")
	}

	first := strings.ToUpper(parts[0])
	if first == "T" || first == "F" || first == "TRUE" || first == "FALSE" {
		field := data.NewFieldFromFieldType(data.FieldTypeNullableBool, len(parts))
		for idx, strVal := range parts {
			strVal = strings.ToUpper(strVal)
			if strVal == "NULL" || strVal == "" {
				continue
			}
			field.SetConcrete(idx, strVal == "T" || strVal == "TRUE")
		}
		return field, nil
	}

	// Try parsing values as floats
	ok := false
	field := data.NewFieldFromFieldType(data.FieldTypeNullableFloat64, len(parts))
	for idx, strVal := range parts {
		if strVal == "null" || strVal == "" {
			continue
		}

		val, err := strconv.ParseFloat(strVal, 64)
		if err != nil {
			ok = false
			break
		}
		field.SetConcrete(idx, val)
		ok = true
	}
	if ok {
		return field, nil
	}

	// Replace empty strings with null
	field = data.NewFieldFromFieldType(data.FieldTypeNullableString, len(parts))
	for idx, strVal := range parts {
		if strVal == "null" || strVal == "" {
			continue
		}
		field.SetConcrete(idx, strVal)
	}
	return field, nil
}

// This will try to convert the values to a timestamp
func toTimeField(field *data.Field) *data.Field {
	found := false
	count := field.Len()
	timeField := data.NewFieldFromFieldType(data.FieldTypeNullableTime, count)
	timeField.Config = field.Config
	timeField.Name = field.Name
	timeField.Labels = field.Labels
	ft := field.Type()
	if ft.Numeric() {
		for i := 0; i < count; i++ {
			v, err := field.FloatAt(i)
			if err == nil {
				t := time.Unix(0, int64(v)*int64(time.Millisecond))
				timeField.SetConcrete(i, t.UTC())
				found = true
			}
		}
		if !found {
			return nil
		}
		return timeField
	}
	if ft == data.FieldTypeNullableString || ft == data.FieldTypeString {
		for i := 0; i < count; i++ {
			v, ok := field.ConcreteAt(i)
			if ok && v != nil {
				t, err := time.Parse(time.RFC3339, v.(string))
				if err == nil {
					timeField.SetConcrete(i, t.UTC())
					found = true
				}
			}
		}
		if !found {
			return nil
		}
		return timeField
	}
	return nil
}
