package schedule

import (
	"cmp"
	"slices"
	"strings"

	models "github.com/grafana/grafana/pkg/services/ngalert/models"
)

// sequence represents a chain of rules that should be evaluated in order.
// It is a convience type that wraps readyToRunItem as an indicator of what
// is being represented.
type sequence readyToRunItem

type groupKey struct {
	folderTitle string
	folderUID   string
	groupName   string
}

// buildSequences organizes rules into evaluation sequences where rules in the same group
// are chained together. The first rule in each group will trigger the evaluation of subsequent
// rules in that group through the afterEval callback.
//
// For example, if we have rules A, B, C in group G1 and rules D, E in group G2:
// - A will have afterEval set to evaluate B
// - B will have afterEval set to evaluate C
// - D will have afterEval set to evaluate E
//
// The function returns a slice of sequences, where each sequence represents a chain of rules
// that should be evaluated in order.
//
// NOTE: This currently only chains rules in imported groups.
func (sch *schedule) buildSequences(items []readyToRunItem, runJobFn func(next readyToRunItem, prev ...readyToRunItem) func()) []sequence {
	// Step 1: Group rules by their folder and group name
	groups := map[groupKey][]readyToRunItem{}
	var keys []groupKey
	for _, item := range items {
		g := groupKey{
			folderTitle: item.folderTitle,
			folderUID:   item.rule.NamespaceUID,
			groupName:   item.rule.RuleGroup,
		}
		i, ok := groups[g]
		if !ok {
			keys = append(keys, g)
		}
		groups[g] = append(i, item)
	}

	// Step 2: Sort group keys to ensure consistent ordering
	slices.SortFunc(keys, func(a, b groupKey) int {
		return cmp.Or(
			cmp.Compare(a.folderTitle, b.folderTitle),
			cmp.Compare(a.folderUID, b.folderUID),
			cmp.Compare(a.groupName, b.groupName),
		)
	})

	// Step 3: Build evaluation sequences for each group
	result := make([]sequence, 0, len(items))
	for _, key := range keys {
		groupItems := groups[key]

		if sch.shouldEvaluateSequentially(groupItems) {
			result = append(result, sch.buildSequence(key, groupItems, runJobFn))
			continue
		}

		for _, item := range groupItems {
			result = append(result, sequence(item))
		}
	}

	// sort the sequences by UID
	slices.SortFunc(result, func(a, b sequence) int {
		return strings.Compare(a.rule.UID, b.rule.UID)
	})

	return result
}

func (sch *schedule) buildSequence(groupKey groupKey, groupItems []readyToRunItem, runJobFn func(next readyToRunItem, prev ...readyToRunItem) func()) sequence {
	if len(groupItems) < 2 {
		return sequence(groupItems[0])
	}

	slices.SortFunc(groupItems, func(a, b readyToRunItem) int {
		return models.RulesGroupComparer(a.rule, b.rule)
	})

	// iterate over the group items backwards to set the afterEval callback
	for i := len(groupItems) - 2; i >= 0; i-- {
		groupItems[i].afterEval = runJobFn(groupItems[i+1], groupItems[i])
	}

	uids := make([]string, 0, len(groupItems))
	for _, item := range groupItems {
		uids = append(uids, item.rule.UID)
	}
	sch.log.Debug("Sequence created", "folder", groupKey.folderTitle, "group", groupKey.groupName, "sequence", strings.Join(uids, "->"))

	return sequence(groupItems[0])
}

func (sch *schedule) shouldEvaluateSequentially(groupItems []readyToRunItem) bool {
	// the no group group shouldn't be evaluated sequentially
	if len(groupItems) > 0 && models.IsNoGroupRuleGroup(groupItems[0].rule.RuleGroup) {
		return false
	}

	// if jitter by rule is enabled, we can't evaluate rules sequentially
	if sch.jitterEvaluations == JitterByRule {
		return false
	}

	// if there is only one rule, there are no rules to chain
	if len(groupItems) == 1 {
		return false
	}

	// only evaluate rules in imported groups sequentially
	for _, item := range groupItems {
		if item.rule.ImportedPrometheusRule() {
			return true
		}
	}

	// default to false
	return false
}
