package kapacitor

import (
	"fmt"
	"sort"
	"strconv"
	"strings"

	"github.com/influxdata/influxdb/v2/chronograf"
)

var (
	// Database is the output database for alerts.
	Database = "chronograf"
	// RP will be autogen for alerts because it is default.
	RP = "autogen"
	// Measurement will be alerts so that the app knows where to get this data.
	Measurement = "alerts"
	// IDTag is the output tag key for the ID of the alert
	IDTag = "alertID"
	//LevelTag is the output tag key for the alert level information
	LevelTag = "level"
	// MessageField is the output field key for the message in the alert
	MessageField = "message"
	// DurationField is the output field key for the duration of the alert
	DurationField = "duration"
)

// Vars builds the top level vars for a kapacitor alert script
func Vars(rule chronograf.AlertRule) (string, error) {
	common, err := commonVars(rule)
	if err != nil {
		return "", err
	}

	switch rule.Trigger {
	case Threshold:
		if rule.TriggerValues.RangeValue == "" {
			vars := `
		%s
        var crit = %s
 `
			return fmt.Sprintf(vars, common, formatValue(rule.TriggerValues.Value)), nil
		}
		vars := `
			%s
					var lower = %s
					var upper = %s
`
		return fmt.Sprintf(vars,
			common,
			rule.TriggerValues.Value,
			rule.TriggerValues.RangeValue), nil
	case Relative:
		vars := `
		%s
        var shift = %s
        var crit = %s
 `
		return fmt.Sprintf(vars,
			common,
			rule.TriggerValues.Shift,
			rule.TriggerValues.Value,
		), nil
	case Deadman:
		vars := `
		%s
        var threshold = %s
 `
		return fmt.Sprintf(vars,
			common,
			"0.0", // deadman threshold hardcoded to zero
		), nil
	default:
		return "", fmt.Errorf("unknown trigger mechanism")
	}
}

// NotEmpty is an error collector checking if strings are empty values
type NotEmpty struct {
	Err error
}

// Valid checks if string s is empty and if so reports an error using name
func (n *NotEmpty) Valid(name, s string) error {
	if n.Err != nil {
		return n.Err

	}
	if s == "" {
		n.Err = fmt.Errorf("%s cannot be an empty string", name)
	}
	return n.Err
}

// Escape sanitizes strings with single quotes for kapacitor
func Escape(str string) string {
	return strings.Replace(str, "'", `\'`, -1)
}

func commonVars(rule chronograf.AlertRule) (string, error) {
	n := new(NotEmpty)
	n.Valid("database", rule.Query.Database)
	n.Valid("retention policy", rule.Query.RetentionPolicy)
	n.Valid("measurement", rule.Query.Measurement)
	n.Valid("alert name", rule.Name)
	n.Valid("trigger type", rule.Trigger)
	if n.Err != nil {
		return "", n.Err
	}

	wind, err := window(rule)
	if err != nil {
		return "", err
	}

	common := `
        var db = '%s'
        var rp = '%s'
        var measurement = '%s'
        var groupBy = %s
        var whereFilter = %s
		%s

		var name = '%s'
		var idVar = %s
		var message = '%s'
		var idTag = '%s'
		var levelTag = '%s'
		var messageField = '%s'
		var durationField = '%s'

        var outputDB = '%s'
        var outputRP = '%s'
        var outputMeasurement = '%s'
        var triggerType = '%s'
    `
	res := fmt.Sprintf(common,
		Escape(rule.Query.Database),
		Escape(rule.Query.RetentionPolicy),
		Escape(rule.Query.Measurement),
		groupBy(rule.Query),
		whereFilter(rule.Query),
		wind,
		Escape(rule.Name),
		idVar(rule.Query),
		Escape(rule.Message),
		IDTag,
		LevelTag,
		MessageField,
		DurationField,
		Database,
		RP,
		Measurement,
		rule.Trigger,
	)

	if rule.Details != "" {
		res += fmt.Sprintf(`
        var details = '%s'
    `, rule.Details)
	}
	return res, nil
}

// window is only used if deadman or threshold/relative with aggregate.  Will return empty
// if no period.
func window(rule chronograf.AlertRule) (string, error) {
	if rule.Trigger == Deadman {
		if rule.TriggerValues.Period == "" {
			return "", fmt.Errorf("period cannot be an empty string in deadman alert")
		}
		return fmt.Sprintf("var period = %s", rule.TriggerValues.Period), nil

	}
	// Period only makes sense if the field has a been grouped via a time duration.
	for _, field := range rule.Query.Fields {
		if field.Type == "func" {
			n := new(NotEmpty)
			n.Valid("group by time", rule.Query.GroupBy.Time)
			n.Valid("every", rule.Every)
			if n.Err != nil {
				return "", n.Err
			}
			return fmt.Sprintf("var period = %s\nvar every = %s", rule.Query.GroupBy.Time, rule.Every), nil
		}
	}
	return "", nil
}

func groupBy(q *chronograf.QueryConfig) string {
	groups := []string{}
	if q != nil {
		for _, tag := range q.GroupBy.Tags {
			groups = append(groups, fmt.Sprintf("'%s'", tag))
		}
	}
	return "[" + strings.Join(groups, ",") + "]"
}

func idVar(q *chronograf.QueryConfig) string {
	if len(q.GroupBy.Tags) > 0 {
		return `name + ':{{.Group}}'`
	}
	return "name"
}

func field(q *chronograf.QueryConfig) (string, error) {
	if q == nil {
		return "", fmt.Errorf("no fields set in query")
	}
	if len(q.Fields) != 1 {
		return "", fmt.Errorf("expect only one field but found %d", len(q.Fields))
	}
	field := q.Fields[0]
	if field.Type == "func" {
		for _, arg := range field.Args {
			if arg.Type == "field" {
				f, ok := arg.Value.(string)
				if !ok {
					return "", fmt.Errorf("field value %v is should be string but is %T", arg.Value, arg.Value)
				}
				return f, nil
			}
		}
		return "", fmt.Errorf("no fields set in query")
	}
	f, ok := field.Value.(string)
	if !ok {
		return "", fmt.Errorf("field value %v is should be string but is %T", field.Value, field.Value)
	}
	return f, nil
}

func whereFilter(q *chronograf.QueryConfig) string {
	if q != nil {
		operator := "=="
		if !q.AreTagsAccepted {
			operator = "!="
		}

		outer := []string{}
		for tag, values := range q.Tags {
			inner := []string{}
			for _, value := range values {
				inner = append(inner, fmt.Sprintf(`"%s" %s '%s'`, tag, operator, value))
			}
			outer = append(outer, "("+strings.Join(inner, " OR ")+")")
		}
		if len(outer) > 0 {
			sort.Strings(outer)
			return "lambda: " + strings.Join(outer, " AND ")
		}
	}
	return "lambda: TRUE"
}

// formatValue return the same string if a numeric type or if it is a string
// will return it as a kapacitor formatted single-quoted string
func formatValue(value string) string {
	// Test if numeric if it can be converted to a float
	if _, err := strconv.ParseFloat(value, 64); err == nil {
		return value
	}

	// If the value is a kapacitor boolean value perform no formatting
	if value == "TRUE" || value == "FALSE" {
		return value
	}
	return "'" + Escape(value) + "'"
}
