package notifier

import (
	"context"
	"errors"
	"fmt"
	"maps"
	"slices"

	"github.com/grafana/grafana-plugin-sdk-go/data"
	"github.com/prometheus/alertmanager/pkg/labels"

	"github.com/grafana/grafana/pkg/infra/log"
	"github.com/grafana/grafana/pkg/services/featuremgmt"
	"github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
	"github.com/grafana/grafana/pkg/services/ngalert/models"
	"github.com/grafana/grafana/pkg/services/ngalert/notifier/legacy_storage"
)

type autogenRuleStore interface {
	ListContactPointRoutings(ctx context.Context, q models.ListContactPointRoutingsQuery) (map[models.AlertRuleKey]models.ContactPointRouting, error)
}

type InvalidReceiversAction string

const (
	ErrorOnInvalidReceivers InvalidReceiversAction = "error"
	LogInvalidReceivers     InvalidReceiversAction = "log"
	IgnoreInvalidReceivers  InvalidReceiversAction = "ignore"
)

// AddAutogenConfig creates the autogenerated configuration and adds it to the given apiAlertingConfig.
// If skipInvalid is true, then invalid notification settings are skipped, otherwise an error is returned.
func AddAutogenConfig[R receiver](ctx context.Context, logger log.Logger, store autogenRuleStore, orgId int64, cfg apiAlertingConfig[R], invalidReceiverAction InvalidReceiversAction, features featuremgmt.FeatureToggles) error {
	autogenRoute, err := newAutogeneratedRoute(ctx, logger, store, orgId, cfg, invalidReceiverAction, features)
	if err != nil {
		return err
	}

	err = autogenRoute.addToRoute(cfg.GetRoute())
	if err != nil {
		return err
	}

	return nil
}

// newAutogeneratedRoute creates a new autogenerated route based on the notification settings for the given org.
// cfg is used to construct the settings validator and to ensure we create a dedicated route for each receiver.
// skipInvalid is used to skip invalid settings instead of returning an error.
func newAutogeneratedRoute[R receiver](ctx context.Context, logger log.Logger, store autogenRuleStore, orgId int64, cfg apiAlertingConfig[R], invalidReceiverAction InvalidReceiversAction, features featuremgmt.FeatureToggles) (autogeneratedRoute, error) {
	settings, err := store.ListContactPointRoutings(ctx, models.ListContactPointRoutingsQuery{OrgID: orgId})
	if err != nil {
		return autogeneratedRoute{}, fmt.Errorf("failed to list alert rules: %w", err)
	}

	notificationSettings := make(map[data.Fingerprint]models.ContactPointRouting)
	// Add a default notification setting for each contact point. This is to ensure that we always have a route for each
	// contact point even if no rules are using it. This will prevent race conditions between AM sync and rule sync.
	for _, receiver := range cfg.GetReceivers() {
		setting := models.NewDefaultContactPointRouting(receiver.GetName())
		fp := setting.Fingerprint(features)
		notificationSettings[fp] = setting
	}

	validator := NewContactPointRoutingValidator(cfg)
	for ruleKey, setting := range settings {
		// TODO we should register this errors and somehow present to the users or make sure the config is always valid.
		if err = validator.Validate(setting); err != nil {
			switch invalidReceiverAction {
			case ErrorOnInvalidReceivers:
				return autogeneratedRoute{}, fmt.Errorf("invalid notification settings for rule %s: %w", ruleKey.UID, err)
			case LogInvalidReceivers:
				logger.Warn("Rule notification settings are invalid. Skipping", append(ruleKey.LogContext(), "error", err)...)
			case IgnoreInvalidReceivers: // do nothing
			}
			continue
		}
		fp := setting.Fingerprint(features)
		// Keep only unique settings.
		if _, ok := notificationSettings[fp]; ok {
			continue
		}
		notificationSettings[fp] = setting
	}
	if len(notificationSettings) == 0 {
		return autogeneratedRoute{}, nil
	}
	newAutogenRoute, err := generateRouteFromSettings(cfg.GetRoute().Receiver, notificationSettings)
	if err != nil {
		return autogeneratedRoute{}, fmt.Errorf("failed to create autogenerated route: %w", err)
	}
	return newAutogenRoute, nil
}

type autogeneratedRoute struct {
	Route *definitions.Route
}

// generateRouteFromSettings generates a route and fingerprint for this route. The route is a tree of 3 layers:
//  1. with matcher by label models.AutogeneratedRouteLabel equals 'true'.
//  2. with matcher by receiver name.
//  3. with matcher by unique combination of optional settings. It is created only if there are optional settings.
func generateRouteFromSettings(defaultReceiver string, settings map[data.Fingerprint]models.ContactPointRouting) (autogeneratedRoute, error) {
	// sort keys to make sure that the hash we calculate using it is stable
	keys := slices.Sorted(maps.Keys(settings))

	rootMatcher, err := labels.NewMatcher(labels.MatchEqual, models.AutogeneratedRouteLabel, "true")
	if err != nil {
		return autogeneratedRoute{}, err
	}

	autoGenRoot := &definitions.Route{
		Receiver:       defaultReceiver,
		ObjectMatchers: definitions.ObjectMatchers{rootMatcher},
		Continue:       false, // We explicitly don't continue toward user-created routes if this matches.
	}

	receiverRoutes := make(map[string]*definitions.Route)
	for _, fingerprint := range keys {
		s := settings[fingerprint]
		receiverRoute, ok := receiverRoutes[s.Receiver]
		if !ok {
			contactMatcher, err := labels.NewMatcher(labels.MatchEqual, models.AutogeneratedRouteReceiverNameLabel, s.Receiver)
			if err != nil {
				return autogeneratedRoute{}, err
			}
			groupByStr := append([]string{}, models.DefaultNotificationSettingsGroupBy...)
			groupByAll, groupBy := legacy_storage.ToGroupBy(groupByStr...)
			receiverRoute = &definitions.Route{
				Receiver:       s.Receiver,
				ObjectMatchers: definitions.ObjectMatchers{contactMatcher},
				Continue:       false,
				// Since we'll have many rules from different folders using this policy, we ensure it has these necessary groupings.
				GroupByStr: groupByStr,
				GroupBy:    groupBy,
				GroupByAll: groupByAll,
			}
			receiverRoutes[s.Receiver] = receiverRoute
			autoGenRoot.Routes = append(autoGenRoot.Routes, receiverRoute)
		}

		// Do not create hash specific route if all group settings such as mute timings, group_wait, group_interval, etc are default
		if s.IsAllDefault() {
			continue
		}
		settingMatcher, err := labels.NewMatcher(labels.MatchEqual, models.AutogeneratedRouteSettingsHashLabel, fingerprint.String())
		if err != nil {
			return autogeneratedRoute{}, err
		}
		normalized := s.NormalizedGroupBy()
		groupByAll, groupBy := legacy_storage.ToGroupBy(normalized...)
		receiverRoute.Routes = append(receiverRoute.Routes, &definitions.Route{
			Receiver:       s.Receiver,
			ObjectMatchers: definitions.ObjectMatchers{settingMatcher},
			Continue:       false, // Only a single setting-specific route should match.

			GroupByStr:          normalized,
			GroupBy:             groupBy,
			GroupByAll:          groupByAll,
			MuteTimeIntervals:   s.MuteTimeIntervals,
			ActiveTimeIntervals: s.ActiveTimeIntervals,
			GroupWait:           s.GroupWait,
			GroupInterval:       s.GroupInterval,
			RepeatInterval:      s.RepeatInterval,
		})
	}

	return autogeneratedRoute{
		Route: autoGenRoot,
	}, nil
}

// addToRoute adds this autogenerated route to the given route as the first top-level route under the root.
func (ar *autogeneratedRoute) addToRoute(route *definitions.Route) error {
	if route == nil {
		return errors.New("route does not exist")
	}
	if ar == nil || ar.Route == nil {
		return nil
	}
	// Combine autogenerated route with the user-created route.
	ar.Route.Receiver = route.Receiver

	// Remove existing autogenerated route if it exists.
	RemoveAutogenConfigIfExists(route)

	route.Routes = append([]*definitions.Route{ar.Route}, route.Routes...)
	return nil
}

// RemoveAutogenConfigIfExists removes all top-level autogenerated routes from the provided route.
// If no autogenerated routes exist, this function does nothing.
func RemoveAutogenConfigIfExists(route *definitions.Route) {
	route.Routes = slices.DeleteFunc(route.Routes, func(route *definitions.Route) bool {
		return IsAutogeneratedRoot(route)
	})
}

// IsAutogeneratedRoot returns true if the route is the root of an autogenerated route.
func IsAutogeneratedRoot(route *definitions.Route) bool {
	return len(route.ObjectMatchers) == 1 && route.ObjectMatchers[0].Name == models.AutogeneratedRouteLabel
}
