package notifier

import (
	"bytes"
	"fmt"
	"io"
	"strings"
	textTpl "text/template"
	"time"

	"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/templates"
	"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/utils"
	"github.com/VictoriaMetrics/VictoriaMetrics/lib/prompbmarshal"
	"github.com/VictoriaMetrics/VictoriaMetrics/lib/promrelabel"
)

// Alert the triggered alert
// TODO: Looks like alert name isn't unique
type Alert struct {
	// GroupID contains the ID of the parent rules group
	GroupID uint64
	// Name represents Alert name
	Name string
	// Labels is the list of label-value pairs attached to the Alert
	Labels map[string]string
	// Annotations is the list of annotations generated on Alert evaluation
	Annotations map[string]string
	// State represents the current state of the Alert
	State AlertState
	// Expr contains expression that was executed to generate the Alert
	Expr string
	// ActiveAt defines the moment of time when Alert has become active
	ActiveAt time.Time
	// Start defines the moment of time when Alert has become firing
	Start time.Time
	// End defines the moment of time when Alert supposed to expire
	End time.Time
	// ResolvedAt defines the moment when Alert was switched from Firing to Inactive
	ResolvedAt time.Time
	// LastSent defines the moment when Alert was sent last time
	LastSent time.Time
	// Value stores the value returned from evaluating expression from Expr field
	Value float64
	// ID is the unique identifer for the Alert
	ID uint64
	// Restored is true if Alert was restored after restart
	Restored bool
}

// AlertState type indicates the Alert state
type AlertState int

const (
	// StateInactive is the state of an alert that is neither firing nor pending.
	StateInactive AlertState = iota
	// StatePending is the state of an alert that has been active for less than
	// the configured threshold duration.
	StatePending
	// StateFiring is the state of an alert that has been active for longer than
	// the configured threshold duration.
	StateFiring
)

// String stringer for AlertState
func (as AlertState) String() string {
	switch as {
	case StateFiring:
		return "firing"
	case StatePending:
		return "pending"
	}
	return "inactive"
}

// AlertTplData is used to execute templating
type AlertTplData struct {
	Labels map[string]string
	Value  float64
	Expr   string
}

var tplHeaders = []string{
	"{{ $value := .Value }}",
	"{{ $labels := .Labels }}",
	"{{ $expr := .Expr }}",
	"{{ $externalLabels := .ExternalLabels }}",
	"{{ $externalURL := .ExternalURL }}",
}

// ExecTemplate executes the Alert template for given
// map of annotations.
// Every alert could have a different datasource, so function
// requires a queryFunction as an argument.
func (a *Alert) ExecTemplate(q templates.QueryFn, labels, annotations map[string]string) (map[string]string, error) {
	tplData := AlertTplData{Value: a.Value, Labels: labels, Expr: a.Expr}
	tmpl, err := templates.GetWithFuncs(templates.FuncsWithQuery(q))
	if err != nil {
		return nil, fmt.Errorf("error getting a template: %w", err)
	}
	return templateAnnotations(annotations, tplData, tmpl, true)
}

// ExecTemplate executes the given template for given annotations map.
func ExecTemplate(q templates.QueryFn, annotations map[string]string, tplData AlertTplData) (map[string]string, error) {
	tmpl, err := templates.GetWithFuncs(templates.FuncsWithQuery(q))
	if err != nil {
		return nil, fmt.Errorf("error cloning template: %w", err)
	}
	return templateAnnotations(annotations, tplData, tmpl, true)
}

// ValidateTemplates validate annotations for possible template error, uses empty data for template population
func ValidateTemplates(annotations map[string]string) error {
	tmpl, err := templates.Get()
	if err != nil {
		return err
	}
	_, err = templateAnnotations(annotations, AlertTplData{
		Labels: map[string]string{},
		Value:  0,
	}, tmpl, false)
	return err
}

func templateAnnotations(annotations map[string]string, data AlertTplData, tmpl *textTpl.Template, execute bool) (map[string]string, error) {
	var builder strings.Builder
	var buf bytes.Buffer
	eg := new(utils.ErrGroup)
	r := make(map[string]string, len(annotations))
	tData := tplData{data, externalLabels, externalURL}
	header := strings.Join(tplHeaders, "")
	for key, text := range annotations {
		buf.Reset()
		builder.Reset()
		builder.Grow(len(header) + len(text))
		builder.WriteString(header)
		builder.WriteString(text)
		if err := templateAnnotation(&buf, builder.String(), tData, tmpl, execute); err != nil {
			r[key] = text
			eg.Add(fmt.Errorf("key %q, template %q: %w", key, text, err))
			continue
		}
		r[key] = buf.String()
	}
	return r, eg.Err()
}

type tplData struct {
	AlertTplData
	ExternalLabels map[string]string
	ExternalURL    string
}

func templateAnnotation(dst io.Writer, text string, data tplData, tmpl *textTpl.Template, execute bool) error {
	tpl, err := tmpl.Clone()
	if err != nil {
		return fmt.Errorf("error cloning template before parse annotation: %w", err)
	}
	tpl, err = tpl.Parse(text)
	if err != nil {
		return fmt.Errorf("error parsing annotation template: %w", err)
	}
	if !execute {
		return nil
	}
	if err = tpl.Execute(dst, data); err != nil {
		return fmt.Errorf("error evaluating annotation template: %w", err)
	}
	return nil
}

func (a Alert) toPromLabels(relabelCfg *promrelabel.ParsedConfigs) []prompbmarshal.Label {
	var labels []prompbmarshal.Label
	for k, v := range a.Labels {
		labels = append(labels, prompbmarshal.Label{
			Name:  k,
			Value: v,
		})
	}
	promrelabel.SortLabels(labels)
	if relabelCfg != nil {
		return relabelCfg.Apply(labels, 0, false)
	}
	return labels
}