package models

import (
	"encoding/binary"
	"errors"
	"hash/fnv"
	"slices"
	"unsafe"

	"github.com/grafana/grafana-plugin-sdk-go/data"
	"github.com/grafana/grafana/pkg/services/featuremgmt"
	"github.com/prometheus/common/model"
)

// GroupByAll is a special value defined by alertmanager that can be used in a Route's GroupBy field to aggregate by all possible labels.
const GroupByAll = "..."

// DefaultNotificationSettingsGroupBy are the default required GroupBy fields for notification settings.
var DefaultNotificationSettingsGroupBy = []string{FolderTitleLabel, model.AlertNameLabel}

type ListNotificationSettingsQuery struct {
	OrgID            int64
	ReceiverName     string
	TimeIntervalName string
}

// NotificationSettings represents the settings for sending notifications for a single AlertRule. It is used to
// automatically generate labels and an associated matching route containing the given settings.
type NotificationSettings struct {
	Receiver string `json:"receiver"`

	GroupBy             []string        `json:"group_by,omitempty"`
	GroupWait           *model.Duration `json:"group_wait,omitempty"`
	GroupInterval       *model.Duration `json:"group_interval,omitempty"`
	RepeatInterval      *model.Duration `json:"repeat_interval,omitempty"`
	MuteTimeIntervals   []string        `json:"mute_time_intervals,omitempty"`
	ActiveTimeIntervals []string        `json:"active_time_intervals,omitempty"`
}

func (s *NotificationSettings) GetUID() string {
	return NameToUid(s.Receiver)
}

// NormalizedGroupBy returns a consistent and ordered GroupBy.
//   - If the GroupBy is empty, it returns nil so that the parent group can be inherited.
//   - If the GroupBy contains the special label '...', it returns only '...'.
//   - Otherwise, it returns the default GroupBy labels followed by any custom labels in sorted order.
//
// To ensure consistent and valid generated routes, this should be used instead of GroupBy when generating fingerprints
// or fingerprint-level routes.
func (s *NotificationSettings) NormalizedGroupBy() []string {
	if len(s.GroupBy) == 0 {
		// Inherit group from parent.
		return nil
	}

	defaultGroupBySet := make(map[string]struct{}, len(DefaultNotificationSettingsGroupBy))
	for _, lbl := range DefaultNotificationSettingsGroupBy {
		defaultGroupBySet[lbl] = struct{}{}
	}

	var customLabels []string
	for _, lbl := range s.GroupBy {
		if lbl == GroupByAll {
			return []string{GroupByAll}
		}
		if _, ok := defaultGroupBySet[lbl]; !ok {
			customLabels = append(customLabels, lbl)
		}
	}

	// Sort the custom labels to ensure consistent ordering while keeping the required labels in the front.
	slices.Sort(customLabels)

	normalized := make([]string, 0, len(DefaultNotificationSettingsGroupBy)+len(customLabels))
	normalized = append(normalized, DefaultNotificationSettingsGroupBy...)
	return append(normalized, customLabels...)
}

// Validate checks if the NotificationSettings object is valid.
// It returns an error if any of the validation checks fail.
// The receiver must be specified.
// GroupWait, GroupInterval, RepeatInterval must be positive durations.
func (s *NotificationSettings) Validate() error {
	if s.Receiver == "" {
		return errors.New("receiver must be specified")
	}
	if s.GroupWait != nil && *s.GroupWait < 0 {
		return errors.New("group wait must be a positive duration")
	}
	if s.GroupInterval != nil && *s.GroupInterval <= 0 {
		return errors.New("group interval must be greater than zero")
	}
	if s.RepeatInterval != nil && *s.RepeatInterval <= 0 {
		return errors.New("repeat interval must be greater than zero")
	}
	return nil
}

// ToLabels converts the NotificationSettings into data.Labels. When added to an AlertRule these labels ensure it will
// match an autogenerated route with the correct settings.
// Labels returned:
//   - AutogeneratedRouteLabel: "true"
//   - AutogeneratedRouteReceiverNameLabel: Receiver
//   - AutogeneratedRouteSettingsHashLabel: Fingerprint (if the NotificationSettings are not all default)
func (s *NotificationSettings) ToLabels(features featuremgmt.FeatureToggles) data.Labels {
	result := make(data.Labels, 3)
	result[AutogeneratedRouteLabel] = "true"
	result[AutogeneratedRouteReceiverNameLabel] = s.Receiver
	if !s.IsAllDefault() {
		result[AutogeneratedRouteSettingsHashLabel] = s.Fingerprint(features).String()
	}
	return result
}

func (s *NotificationSettings) Equals(other *NotificationSettings) bool {
	durationEqual := func(d1, d2 *model.Duration) bool {
		if d1 == nil || d2 == nil {
			return d1 == d2
		}
		return *d1 == *d2
	}
	if s == nil || other == nil {
		return s == nil && other == nil
	}
	if s.Receiver != other.Receiver {
		return false
	}
	if !durationEqual(s.GroupWait, other.GroupWait) {
		return false
	}
	if !durationEqual(s.GroupInterval, other.GroupInterval) {
		return false
	}
	if !durationEqual(s.RepeatInterval, other.RepeatInterval) {
		return false
	}
	if !slices.Equal(s.MuteTimeIntervals, other.MuteTimeIntervals) {
		return false
	}
	if !slices.Equal(s.ActiveTimeIntervals, other.ActiveTimeIntervals) {
		return false
	}
	sGr := s.GroupBy
	oGr := other.GroupBy
	return slices.Equal(sGr, oGr)
}

// IsAllDefault checks if the NotificationSettings object has all default values for optional fields (all except Receiver) .
func (s *NotificationSettings) IsAllDefault() bool {
	return len(s.GroupBy) == 0 && s.GroupWait == nil && s.GroupInterval == nil && s.RepeatInterval == nil && len(s.MuteTimeIntervals) == 0 && len(s.ActiveTimeIntervals) == 0
}

// NewDefaultNotificationSettings creates a new default NotificationSettings with the specified receiver.
func NewDefaultNotificationSettings(receiver string) NotificationSettings {
	return NotificationSettings{
		Receiver: receiver,
	}
}

// Fingerprint calculates a hash value to uniquely identify a NotificationSettings by its attributes.
// The hash is calculated by concatenating the strings and durations of the NotificationSettings attributes
// and using an invalid UTF-8 sequence as a separator.
func (s *NotificationSettings) Fingerprint(features featuremgmt.FeatureToggles) data.Fingerprint {
	h := fnv.New64()
	tmp := make([]byte, 8)

	writeString := func(s string) {
		// save on extra slice allocation when string is converted to bytes.
		_, _ = h.Write(unsafe.Slice(unsafe.StringData(s), len(s))) //nolint:gosec
		// ignore errors returned by Write method because fnv never returns them.
		_, _ = h.Write([]byte{255}) // use an invalid utf-8 sequence as separator
	}
	writeDuration := func(d *model.Duration) {
		if d == nil {
			_, _ = h.Write([]byte{255})
		} else {
			binary.LittleEndian.PutUint64(tmp, uint64(*d))
			_, _ = h.Write(tmp)
			_, _ = h.Write([]byte{255})
		}
	}

	writeString(s.Receiver)
	for _, gb := range s.NormalizedGroupBy() {
		writeString(gb)
	}
	writeDuration(s.GroupWait)
	writeDuration(s.GroupInterval)
	writeDuration(s.RepeatInterval)
	for _, interval := range s.MuteTimeIntervals {
		writeString(interval)
	}
	// Add a separator between the time intervals to avoid collisions
	// when all settings are the same including interval names except for the interval type (mute vs active).
	// Use new algorithm by default, unless feature flag is explicitly disabled
	//nolint:staticcheck // not yet migrated to OpenFeature
	if features == nil || (features != nil && features.IsEnabledGlobally(featuremgmt.FlagAlertingUseNewSimplifiedRoutingHashAlgorithm)) {
		_, _ = h.Write([]byte{255})
	}
	for _, interval := range s.ActiveTimeIntervals {
		writeString(interval)
	}

	return data.Fingerprint(h.Sum64())
}
