package cli

import (
	"context"
	"maps"
	"slices"

	"github.com/alecthomas/kingpin/v2"
	"github.com/pkg/errors"

	"github.com/kopia/kopia/notification"
	"github.com/kopia/kopia/notification/notifyprofile"
	"github.com/kopia/kopia/notification/sender"
	"github.com/kopia/kopia/repo"
)

// commonNotificationOptions is a common configuration for notification methods.
type commonNotificationOptions struct {
	notificationProfileFlag
	sendTestNotification bool
	minSeverity          string
}

func (c *commonNotificationOptions) setup(svc appServices, cmd *kingpin.CmdClause) {
	c.notificationProfileFlag.setup(svc, cmd)
	cmd.Flag("send-test-notification", "Test the notification").BoolVar(&c.sendTestNotification)
	cmd.Flag("min-severity", "Minimum severity").EnumVar(&c.minSeverity, mapKeys(notification.SeverityToNumber)...)
}

// configureNotificationAction is a helper function that creates a Kingpin action that
// configures a notification method.
// it will read the existing profile, merge the provided options, and save the profile back
// or send a test notification based on the flags.
func configureNotificationAction[T any](
	svc appServices,
	c *commonNotificationOptions,
	senderMethod sender.Method,
	opt *T,
	merge func(ctx context.Context, src T, dst *T, isUpdate bool) error,
) func(ctx *kingpin.ParseContext) error {
	return svc.directRepositoryWriteAction(func(ctx context.Context, rep repo.DirectRepositoryWriter) error {
		var (
			defaultT      T
			mergedOptions *T
		)

		// read the existing profile, if any.
		oldProfile, err := notifyprofile.GetProfile(ctx, rep, c.profileName)
		if err != nil && !errors.Is(err, notifyprofile.ErrNotFound) {
			return errors.Wrap(err, "unable to get notification profile")
		}

		sev := notification.SeverityDefault
		exists := err == nil

		if exists {
			if oldProfile.MethodConfig.Type != senderMethod {
				return errors.Errorf("profile %q already exists but is not of type %q", c.profileName, senderMethod)
			}

			var parsedT T

			if err := oldProfile.MethodConfig.Options(&parsedT); err != nil {
				return errors.Wrapf(err, "profile %q already exists but is not of type %q", c.profileName, senderMethod)
			}

			mergedOptions = &parsedT
			sev = oldProfile.MinSeverity
		} else {
			mergedOptions = &defaultT
		}

		if err := merge(ctx, *opt, mergedOptions, exists); err != nil {
			return errors.Wrap(err, "unable to merge options")
		}

		if c.minSeverity != "" {
			// severity is specified on the command line, override the one from the profile.
			sev = notification.SeverityToNumber[c.minSeverity]
		}

		s, err := sender.GetSender(ctx, c.profileName, senderMethod, mergedOptions)
		if err != nil {
			return errors.Wrap(err, "unable to get notification provider")
		}

		if c.sendTestNotification {
			if err := notification.SendTestNotification(ctx, rep, s); err != nil {
				return errors.Wrap(err, "unable to send test notification")
			}
		}

		log(ctx).Infof("Saving notification profile %q of type %q with severity %q.", c.profileName, senderMethod, notification.SeverityToString[sev])

		return notifyprofile.SaveProfile(ctx, rep, notifyprofile.Config{
			ProfileName: c.profileName,
			MethodConfig: sender.MethodConfig{
				Type:   senderMethod,
				Config: mergedOptions,
			},
			MinSeverity: sev,
		})
	})
}

func mapKeys[Map ~map[K]V, K comparable, V any](m Map) []K {
	return slices.AppendSeq(make([]K, 0, len(m)), maps.Keys(m))
}
