1package testdatasource
2
3import (
4	"context"
5	"encoding/csv"
6	"errors"
7	"fmt"
8	"io"
9	"os"
10	"path/filepath"
11	"regexp"
12	"strconv"
13	"strings"
14	"time"
15
16	"github.com/grafana/grafana-plugin-sdk-go/backend"
17	"github.com/grafana/grafana-plugin-sdk-go/data"
18	"github.com/grafana/grafana/pkg/components/simplejson"
19)
20
21func (s *Service) handleCsvContentScenario(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
22	resp := backend.NewQueryDataResponse()
23
24	for _, q := range req.Queries {
25		model, err := simplejson.NewJson(q.JSON)
26		if err != nil {
27			return nil, fmt.Errorf("failed to parse query json: %v", err)
28		}
29
30		csvContent := model.Get("csvContent").MustString()
31		alias := model.Get("alias").MustString("")
32
33		frame, err := LoadCsvContent(strings.NewReader(csvContent), alias)
34		if err != nil {
35			return nil, err
36		}
37
38		respD := resp.Responses[q.RefID]
39		respD.Frames = append(respD.Frames, frame)
40		resp.Responses[q.RefID] = respD
41	}
42
43	return resp, nil
44}
45
46func (s *Service) handleCsvFileScenario(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
47	resp := backend.NewQueryDataResponse()
48
49	for _, q := range req.Queries {
50		model, err := simplejson.NewJson(q.JSON)
51		if err != nil {
52			return nil, fmt.Errorf("failed to parse query json %v", err)
53		}
54
55		fileName := model.Get("csvFileName").MustString()
56
57		if len(fileName) == 0 {
58			continue
59		}
60
61		frame, err := s.loadCsvFile(fileName)
62
63		if err != nil {
64			return nil, err
65		}
66
67		respD := resp.Responses[q.RefID]
68		respD.Frames = append(respD.Frames, frame)
69		resp.Responses[q.RefID] = respD
70	}
71
72	return resp, nil
73}
74
75func (s *Service) loadCsvFile(fileName string) (*data.Frame, error) {
76	validFileName := regexp.MustCompile(`^\w+\.csv$`)
77
78	if !validFileName.MatchString(fileName) {
79		return nil, fmt.Errorf("invalid csv file name: %q", fileName)
80	}
81
82	csvFilepath := filepath.Clean(filepath.Join("/", fileName))
83	filePath := filepath.Join(s.cfg.StaticRootPath, "testdata", csvFilepath)
84
85	// Can ignore gosec G304 here, because we check the file pattern above
86	// nolint:gosec
87	fileReader, err := os.Open(filePath)
88	if err != nil {
89		return nil, fmt.Errorf("failed open file: %v", err)
90	}
91
92	defer func() {
93		if err := fileReader.Close(); err != nil {
94			s.logger.Warn("Failed to close file", "err", err, "path", fileName)
95		}
96	}()
97
98	return LoadCsvContent(fileReader, fileName)
99}
100
101// LoadCsvContent should be moved to the SDK
102func LoadCsvContent(ioReader io.Reader, name string) (*data.Frame, error) {
103	reader := csv.NewReader(ioReader)
104
105	// Read the header records
106	headerFields, err := reader.Read()
107	if err != nil {
108		return nil, fmt.Errorf("failed to read header line: %v", err)
109	}
110
111	fields := []*data.Field{}
112	fieldNames := []string{}
113	fieldRawValues := [][]string{}
114
115	for _, fieldName := range headerFields {
116		fieldNames = append(fieldNames, strings.Trim(fieldName, " "))
117		fieldRawValues = append(fieldRawValues, []string{})
118	}
119
120	for {
121		lineValues, err := reader.Read()
122		if errors.Is(err, io.EOF) {
123			break // reached end of the file
124		} else if err != nil {
125			return nil, fmt.Errorf("failed to read line: %v", err)
126		}
127
128		for fieldIndex, value := range lineValues {
129			fieldRawValues[fieldIndex] = append(fieldRawValues[fieldIndex], strings.Trim(value, " "))
130		}
131	}
132
133	longest := 0
134	for fieldIndex, rawValues := range fieldRawValues {
135		fieldName := fieldNames[fieldIndex]
136		field, err := csvValuesToField(rawValues)
137		if err == nil {
138			// Check if the values are actually a time field
139			if strings.Contains(strings.ToLower(fieldName), "time") {
140				timeField := toTimeField(field)
141				if timeField != nil {
142					field = timeField
143				}
144			}
145
146			field.Name = fieldName
147			fields = append(fields, field)
148			if field.Len() > longest {
149				longest = field.Len()
150			}
151		}
152	}
153
154	// Make all fields the same length
155	for _, field := range fields {
156		delta := field.Len() - longest
157		if delta > 0 {
158			field.Extend(delta)
159		}
160	}
161
162	frame := data.NewFrame(name, fields...)
163	return frame, nil
164}
165
166func csvLineToField(stringInput string) (*data.Field, error) {
167	return csvValuesToField(strings.Split(strings.ReplaceAll(stringInput, " ", ""), ","))
168}
169
170func csvValuesToField(parts []string) (*data.Field, error) {
171	if len(parts) < 1 {
172		return nil, fmt.Errorf("csv must have at least one value")
173	}
174
175	first := strings.ToUpper(parts[0])
176	if first == "T" || first == "F" || first == "TRUE" || first == "FALSE" {
177		field := data.NewFieldFromFieldType(data.FieldTypeNullableBool, len(parts))
178		for idx, strVal := range parts {
179			strVal = strings.ToUpper(strVal)
180			if strVal == "NULL" || strVal == "" {
181				continue
182			}
183			field.SetConcrete(idx, strVal == "T" || strVal == "TRUE")
184		}
185		return field, nil
186	}
187
188	// Try parsing values as numbers
189	ok := false
190	field := data.NewFieldFromFieldType(data.FieldTypeNullableInt64, len(parts))
191	for idx, strVal := range parts {
192		if strVal == "null" || strVal == "" {
193			continue
194		}
195
196		val, err := strconv.ParseInt(strVal, 10, 64)
197		if err != nil {
198			ok = false
199			break
200		}
201		field.SetConcrete(idx, val)
202		ok = true
203	}
204	if ok {
205		return field, nil
206	}
207
208	// Maybe floats
209	field = data.NewFieldFromFieldType(data.FieldTypeNullableFloat64, len(parts))
210	for idx, strVal := range parts {
211		if strVal == "null" || strVal == "" {
212			continue
213		}
214
215		val, err := strconv.ParseFloat(strVal, 64)
216		if err != nil {
217			ok = false
218			break
219		}
220		field.SetConcrete(idx, val)
221		ok = true
222	}
223	if ok {
224		return field, nil
225	}
226
227	// Replace empty strings with null
228	field = data.NewFieldFromFieldType(data.FieldTypeNullableString, len(parts))
229	for idx, strVal := range parts {
230		if strVal == "null" || strVal == "" {
231			continue
232		}
233		field.SetConcrete(idx, strVal)
234	}
235	return field, nil
236}
237
238// This will try to convert the values to a timestamp
239func toTimeField(field *data.Field) *data.Field {
240	found := false
241	count := field.Len()
242	timeField := data.NewFieldFromFieldType(data.FieldTypeNullableTime, count)
243	timeField.Config = field.Config
244	timeField.Name = field.Name
245	timeField.Labels = field.Labels
246	ft := field.Type()
247	if ft.Numeric() {
248		for i := 0; i < count; i++ {
249			v, err := field.FloatAt(i)
250			if err == nil {
251				t := time.Unix(0, int64(v)*int64(time.Millisecond))
252				timeField.SetConcrete(i, t.UTC())
253				found = true
254			}
255		}
256		if !found {
257			return nil
258		}
259		return timeField
260	}
261	if ft == data.FieldTypeNullableString || ft == data.FieldTypeString {
262		for i := 0; i < count; i++ {
263			v, ok := field.ConcreteAt(i)
264			if ok && v != nil {
265				t, err := time.Parse(time.RFC3339, v.(string))
266				if err == nil {
267					timeField.SetConcrete(i, t.UTC())
268					found = true
269				}
270			}
271		}
272		if !found {
273			return nil
274		}
275		return timeField
276	}
277	return nil
278}
279