package csv2lp

import (
	"fmt"
	"log"
	"regexp"
	"strconv"
	"strings"
	"time"
)

// annotationComment describes CSV annotation
type annotationComment struct {
	// prefix in a CSV row that recognizes this annotation
	prefix string
	// flag is 0 to represent an annotation that is used for all data rows
	// or a unique bit (>0) between supported annotation prefixes
	flag uint8
	// setupColumn setups metadata that drives the way of how column data
	// are parsed, mandatory when flag > 0
	setupColumn func(column *CsvTableColumn, columnValue string)
	// setupTable setups metadata that drives the way of how the table data
	// are parsed, mandatory when flag == 0
	setupTable func(table *CsvTable, row []string) error
}

// isTableAnnotation returns true for a table-wide annotation, false for column-based annotations
func (a annotationComment) isTableAnnotation() bool {
	return a.setupTable != nil
}

// matches tests whether an annotationComment can process the CSV comment row
func (a annotationComment) matches(comment string) bool {
	return strings.HasPrefix(strings.ToLower(comment), a.prefix)
}

func createConstantOrConcatColumn(table *CsvTable, row []string, annotationName string) CsvTableColumn {
	// adds a virtual column with constant value to all data rows
	// supported types of constant annotation rows are:
	//  1. "#constant,datatype,label,defaultValue"
	//  2. "#constant,measurement,value"
	//  3. "#constant,dateTime,value"
	//  4. "#constant datatype,label,defaultValue"
	//  5. "#constant measurement,value"
	//  6. "#constant dateTime,value"
	// defaultValue is optional, additional columns are ignored
	col := CsvTableColumn{}
	col.Index = -1 // this is a virtual column that never extracts data from data rows
	// setup column data type
	col.setupDataType(row[0])
	var dataTypeIndex int
	if len(col.DataType) == 0 && col.LinePart == 0 {
		// type 1,2,3
		dataTypeIndex = 1
		if len(row) > 1 {
			col.setupDataType(row[1])
		}
	} else {
		// type 4,5,6
		dataTypeIndex = 0
	}
	// setup label if available
	if len(row) > dataTypeIndex+1 {
		col.Label = row[dataTypeIndex+1]
	}
	// setup defaultValue if available
	if len(row) > dataTypeIndex+2 {
		col.DefaultValue = row[dataTypeIndex+2]
	}
	// support type 2,3,5,6 syntax for measurement and timestamp
	if col.LinePart == linePartMeasurement || col.LinePart == linePartTime {
		if col.DefaultValue == "" && col.Label != "" {
			// type 2,3,5,6
			col.DefaultValue = col.Label
			col.Label = annotationName + " " + col.DataType
		} else if col.Label == "" {
			// setup a label if no label is supplied for focused error messages
			col.Label = annotationName + " " + col.DataType
		}
	}
	// add a virtual column to the table
	return col
}

// constantSetupTable setups the supplied CSV table from #constant annotation
func constantSetupTable(table *CsvTable, row []string) error {
	col := createConstantOrConcatColumn(table, row, "#constant")
	// add a virtual column to the table
	table.extraColumns = append(table.extraColumns, &col)
	return nil
}

// computedReplacer is used to replace value in computed columns
var computedReplacer *regexp.Regexp = regexp.MustCompile(`\$\{[^}]+\}`)

// concatSetupTable setups the supplied CSV table from #concat annotation
func concatSetupTable(table *CsvTable, row []string) error {
	col := createConstantOrConcatColumn(table, row, "#concat")
	template := col.DefaultValue
	col.ComputeValue = func(row []string) string {
		return computedReplacer.ReplaceAllStringFunc(template, func(text string) string {
			columnLabel := text[2 : len(text)-1] // ${columnLabel}
			if placeholderColumn := table.Column(columnLabel); placeholderColumn != nil {
				return placeholderColumn.Value(row)
			}
			log.Printf("WARNING: column %s: column '%s' cannot be replaced, no such column available", col.Label, columnLabel)
			return ""
		})
	}
	// add a virtual column to the table
	table.extraColumns = append(table.extraColumns, &col)
	// add validator to report error when no placeholder column is available
	table.validators = append(table.validators, func(table *CsvTable) error {
		placeholders := computedReplacer.FindAllString(template, len(template))
		for _, placeholder := range placeholders {
			columnLabel := placeholder[2 : len(placeholder)-1] // ${columnLabel}
			if placeholderColumn := table.Column(columnLabel); placeholderColumn == nil {
				return CsvColumnError{
					Column: col.Label,
					Err: fmt.Errorf("'%s' references an unknown column '%s', available columns are: %v",
						template, columnLabel, strings.Join(table.ColumnLabels(), ",")),
				}
			}
		}
		return nil
	})
	return nil
}

// supportedAnnotations contains all supported CSV annotations comments
var supportedAnnotations = []annotationComment{
	{
		prefix: "#group",
		flag:   1,
		setupColumn: func(column *CsvTableColumn, value string) {
			// standard flux query result annotation
			if strings.HasSuffix(value, "true") {
				// setup column's line part unless it is already set (#19452)
				if column.LinePart == 0 {
					column.LinePart = linePartTag
				}
			}
		},
	},
	{
		prefix: "#datatype",
		flag:   2,
		setupColumn: func(column *CsvTableColumn, value string) {
			// standard flux query result annotation
			column.setupDataType(value)
		},
	},
	{
		prefix: "#default",
		flag:   4,
		setupColumn: func(column *CsvTableColumn, value string) {
			// standard flux query result annotation
			column.DefaultValue = ignoreLeadingComment(value)
		},
	},
	{
		prefix:     "#constant",
		setupTable: constantSetupTable,
	},
	{
		prefix: "#timezone",
		setupTable: func(table *CsvTable, row []string) error {
			// setup timezone for parsing timestamps, UTC by default
			val := ignoreLeadingComment(row[0])
			if val == "" && len(row) > 1 {
				val = row[1] // #timezone,Local
			}
			tz, err := parseTimeZone(val)
			if err != nil {
				return fmt.Errorf("#timezone annotation: %v", err)
			}
			table.timeZone = tz
			return nil
		},
	},
	{
		prefix:     "#concat",
		setupTable: concatSetupTable,
	},
}

// ignoreLeadingComment returns a value without '#anyComment ' prefix
func ignoreLeadingComment(value string) string {
	if len(value) > 0 && value[0] == '#' {
		pos := strings.Index(value, " ")
		if pos > 0 {
			return strings.TrimLeft(value[pos+1:], " ")
		}
		return ""
	}
	return value
}

// parseTimeZone parses the supplied timezone from a string into a time.Location
//
//	parseTimeZone("")      // time.UTC
//	parseTimeZone("local") // time.Local
//	parseTimeZone("-0500") // time.FixedZone(-5*3600 + 0*60)
//	parseTimeZone("+0200") // time.FixedZone(2*3600 + 0*60)
//	parseTimeZone("EST")   // time.LoadLocation("EST")
func parseTimeZone(val string) (*time.Location, error) {
	switch {
	case val == "":
		return time.UTC, nil
	case strings.ToLower(val) == "local":
		return time.Local, nil
	case val[0] == '-' || val[0] == '+':
		if matched, _ := regexp.MatchString("[+-][0-9][0-9][0-9][0-9]", val); !matched {
			return nil, fmt.Errorf("timezone '%s' is not  +hhmm or -hhmm", val)
		}
		intVal, _ := strconv.Atoi(val)
		offset := (intVal/100)*3600 + (intVal%100)*60
		return time.FixedZone(val, offset), nil
	default:
		return time.LoadLocation(val)
	}
}
