package rule

import (
	"context"
	"errors"
	"fmt"
	"strings"
	"time"

	"github.com/VictoriaMetrics/VictoriaLogs/lib/logstorage"
	"github.com/VictoriaMetrics/metrics"

	"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/config"
	"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/datasource"
	"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/vmalertutil"
	"github.com/VictoriaMetrics/VictoriaMetrics/lib/decimal"
	"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
	"github.com/VictoriaMetrics/VictoriaMetrics/lib/prompb"
	"github.com/VictoriaMetrics/VictoriaMetrics/lib/promrelabel"
)

// RecordingRule is a Rule that supposed
// to evaluate configured Expression and
// return TimeSeries as result.
type RecordingRule struct {
	Type      config.Type
	RuleID    uint64
	Name      string
	Expr      string
	Labels    map[string]string
	GroupID   uint64
	GroupName string
	File      string
	Debug     bool

	q datasource.Querier

	// state stores recent state changes
	// during evaluations
	state *ruleState

	lastEvaluation map[string]struct{}

	metrics *recordingRuleMetrics
}

type recordingRuleMetrics struct {
	errors  *vmalertutil.Counter
	samples *vmalertutil.Gauge
}

func newRecordingRuleMetrics(set *metrics.Set, rr *RecordingRule) *recordingRuleMetrics {
	rmr := &recordingRuleMetrics{}

	labels := fmt.Sprintf(`recording=%q, group=%q, file=%q, id="%d"`, rr.Name, rr.GroupName, rr.File, rr.ID())
	rmr.errors = vmalertutil.NewCounter(set, fmt.Sprintf(`vmalert_recording_rules_errors_total{%s}`, labels))
	rmr.samples = vmalertutil.NewGauge(set, fmt.Sprintf(`vmalert_recording_rules_last_evaluation_samples{%s}`, labels),
		func() float64 {
			e := rr.state.getLast()
			return float64(e.Samples)
		})

	return rmr
}

func (m *recordingRuleMetrics) close() {
	if m == nil {
		return
	}
	m.errors.Unregister()
	m.samples.Unregister()
}

// String implements Stringer interface
func (rr *RecordingRule) String() string {
	return rr.Name
}

// ID returns unique Rule ID
// within the parent Group.
func (rr *RecordingRule) ID() uint64 {
	return rr.RuleID
}

// ToAPI returns ApiRule representation of rr
func (rr *RecordingRule) ToAPI() ApiRule {
	state := rr.state
	lastState := state.getLast()
	r := ApiRule{
		Type:              TypeRecording,
		DatasourceType:    rr.Type.String(),
		Name:              rr.Name,
		Query:             rr.Expr,
		Labels:            rr.Labels,
		LastEvaluation:    lastState.Time,
		EvaluationTime:    lastState.Duration.Seconds(),
		Health:            "ok",
		LastSamples:       lastState.Samples,
		LastSeriesFetched: lastState.SeriesFetched,
		MaxUpdates:        state.size(),
		Updates:           state.getAll(),

		// encode as strings to avoid rounding
		ID:        fmt.Sprintf("%d", rr.ID()),
		GroupID:   fmt.Sprintf("%d", rr.GroupID),
		GroupName: rr.GroupName,
		File:      rr.File,
	}
	if lastState.Err != nil {
		r.LastError = lastState.Err.Error()
		r.Health = "err"
	}
	return r
}

// NewRecordingRule creates a new RecordingRule
func NewRecordingRule(qb datasource.QuerierBuilder, group *Group, cfg config.Rule) *RecordingRule {
	debug := group.Debug
	if cfg.Debug != nil {
		debug = *cfg.Debug
	}
	rr := &RecordingRule{
		Type:      group.Type,
		RuleID:    cfg.ID,
		Name:      cfg.Record,
		Expr:      cfg.Expr,
		Labels:    cfg.Labels,
		GroupID:   group.GetID(),
		GroupName: group.Name,
		File:      group.File,
		Debug:     debug,
		q: qb.BuildWithParams(datasource.QuerierParams{
			DataSourceType:            group.Type.String(),
			ApplyIntervalAsTimeFilter: setIntervalAsTimeFilter(group.Type.String(), cfg.Expr),
			EvaluationInterval:        group.Interval,
			QueryParams:               group.Params,
			Headers:                   group.Headers,
			Debug:                     debug,
		}),
	}

	entrySize := *ruleUpdateEntriesLimit
	if cfg.UpdateEntriesLimit != nil {
		entrySize = *cfg.UpdateEntriesLimit
	}
	if entrySize < 1 {
		entrySize = 1
	}
	rr.state = &ruleState{
		entries: make([]StateEntry, entrySize),
	}
	return rr
}

func (rr *RecordingRule) registerMetrics(set *metrics.Set) {
	rr.metrics = newRecordingRuleMetrics(set, rr)
}

// close unregisters rule metrics
func (rr *RecordingRule) unregisterMetrics() {
	rr.metrics.close()
}

// execRange executes recording rule on the given time range similarly to Exec.
// It doesn't update internal states of the Rule and meant to be used just
// to get time series for backfilling.
func (rr *RecordingRule) execRange(ctx context.Context, start, end time.Time) ([]prompb.TimeSeries, error) {
	res, err := rr.q.QueryRange(ctx, rr.Expr, start, end)
	if err != nil {
		return nil, err
	}
	duplicates := make(map[string]struct{}, len(res.Data))
	var tss []prompb.TimeSeries
	for _, s := range res.Data {
		ts := rr.toTimeSeries(s)
		key := stringifyLabels(ts.Labels)
		if _, ok := duplicates[key]; ok {
			return nil, fmt.Errorf("original metric %v; resulting labels %q: %w", s.Labels, key, errDuplicate)
		}
		duplicates[key] = struct{}{}
		tss = append(tss, ts)
	}
	return tss, nil
}

// exec executes RecordingRule expression via the given Querier.
func (rr *RecordingRule) exec(ctx context.Context, ts time.Time, limit int) ([]prompb.TimeSeries, error) {
	start := time.Now()
	res, req, err := rr.q.Query(ctx, rr.Expr, ts)
	curState := StateEntry{
		Time:          start,
		At:            ts,
		Duration:      time.Since(start),
		Samples:       len(res.Data),
		SeriesFetched: res.SeriesFetched,
		Curl:          requestToCurl(req),
	}

	defer func() {
		rr.state.add(curState)
		if curState.Err != nil && !errors.Is(curState.Err, context.Canceled) {
			rr.metrics.errors.Inc()
		}
	}()

	if err != nil {
		curState.Err = fmt.Errorf("failed to execute query %q: %w", rr.Expr, err)
		return nil, curState.Err
	}

	rr.logDebugf(ts, "query returned %d samples (elapsed: %s, isPartial: %t)", curState.Samples, curState.Duration, isPartialResponse(res))

	qMetrics := res.Data
	numSeries := len(qMetrics)
	if limit > 0 && numSeries > limit {
		curState.Err = fmt.Errorf("exec exceeded limit of %d with %d series", limit, numSeries)
		return nil, curState.Err
	}

	curEvaluation := make(map[string]struct{}, len(qMetrics))
	lastEvaluation := rr.lastEvaluation
	var tss []prompb.TimeSeries
	for _, r := range qMetrics {
		ts := rr.toTimeSeries(r)
		key := stringifyLabels(ts.Labels)
		if _, ok := curEvaluation[key]; ok {
			curState.Err = fmt.Errorf("original metric %v; resulting labels %q: %w", r, key, errDuplicate)
			return nil, curState.Err
		}
		curEvaluation[key] = struct{}{}
		delete(lastEvaluation, key)
		tss = append(tss, ts)
	}
	// check for stale time series
	for k := range lastEvaluation {
		tss = append(tss, prompb.TimeSeries{
			Labels: stringToLabels(k),
			Samples: []prompb.Sample{
				{Value: decimal.StaleNaN, Timestamp: ts.UnixNano() / 1e6},
			},
		})
	}
	rr.lastEvaluation = curEvaluation
	return tss, nil
}

func (rr *RecordingRule) logDebugf(at time.Time, format string, args ...any) {
	if !rr.Debug {
		return
	}
	prefix := fmt.Sprintf("DEBUG recording rule %q, %q:%q (%d) at %v: ",
		rr.File, rr.GroupName, rr.Name, rr.RuleID, at.Format(time.RFC3339))

	msg := fmt.Sprintf(format, args...)
	logger.Infof("%s", prefix+msg)
}

func stringToLabels(s string) []prompb.Label {
	labels := strings.Split(s, ",")
	rLabels := make([]prompb.Label, 0, len(labels))
	for i := range labels {
		if label := strings.Split(labels[i], "="); len(label) == 2 {
			rLabels = append(rLabels, prompb.Label{
				Name:  label[0],
				Value: label[1],
			})
		}
	}
	return rLabels
}

func stringifyLabels(labels []prompb.Label) string {
	b := strings.Builder{}
	for i, l := range labels {
		b.WriteString(l.Name)
		b.WriteString("=")
		b.WriteString(l.Value)
		if i != len(labels)-1 {
			b.WriteString(",")
		}
	}
	return b.String()
}

func (rr *RecordingRule) toTimeSeries(m datasource.Metric) prompb.TimeSeries {
	if preN := promrelabel.GetLabelByName(m.Labels, "__name__"); preN != nil {
		preN.Value = rr.Name
	} else {
		m.Labels = append(m.Labels, prompb.Label{
			Name:  "__name__",
			Value: rr.Name,
		})
	}
	// add extra labels configured by user
	for k := range rr.Labels {
		// do not add label with empty value to the result, as it has no meaning:
		// if the label already exists in the original query result, remove it to preserve compatibility with relabeling, see https://github.com/VictoriaMetrics/VictoriaMetrics/issues/10766.
		// otherwise, ignore the label, see https://github.com/VictoriaMetrics/VictoriaMetrics/issues/9984.
		if rr.Labels[k] == "" {
			m.DelLabel(k)
			continue
		}
		existingLabel := promrelabel.GetLabelByName(m.Labels, k)
		if existingLabel != nil { // there is a conflict between extra and existing label
			if existingLabel.Value == rr.Labels[k] {
				// extra and existing labels are identical - do nothing
				continue
			}
			// preserve existing label by adding "exported_" prefix
			existingLabel.Name = fmt.Sprintf("exported_%s", existingLabel.Name)
		}
		// add extra label
		m.Labels = append(m.Labels, prompb.Label{
			Name:  k,
			Value: rr.Labels[k],
		})
	}
	ts := newTimeSeries(m.Values, m.Timestamps, m.Labels)
	return ts
}

// updateWith copies all significant fields.
func (rr *RecordingRule) updateWith(r Rule) error {
	nr, ok := r.(*RecordingRule)
	if !ok {
		return fmt.Errorf("BUG: attempt to update recording rule with wrong type %#v", r)
	}
	rr.Expr = nr.Expr
	rr.Labels = nr.Labels
	rr.q = nr.q
	rr.Debug = nr.Debug
	return nil
}

// setIntervalAsTimeFilter returns true if given LogsQL has a time filter.
func setIntervalAsTimeFilter(dType, expr string) bool {
	if dType != "vlogs" {
		return false
	}
	q, err := logstorage.ParseStatsQuery(expr, 0)
	if err != nil {
		logger.Panicf("BUG: the LogsQL query must be valid here; got error: %s; query=[%s]", err, expr)
	}
	return !q.HasGlobalTimeFilter()
}
