package kvstore

import (
	"context"
	"errors"
	"fmt"
	"sync"

	"github.com/grafana/grafana/pkg/infra/kvstore"
	"github.com/grafana/grafana/pkg/infra/log"
	"github.com/grafana/grafana/pkg/plugins"
	smp "github.com/grafana/grafana/pkg/plugins/backendplugin/secretsmanagerplugin"
	"github.com/grafana/grafana/pkg/services/datasources"
	"github.com/grafana/grafana/pkg/services/featuremgmt"
	"github.com/grafana/grafana/pkg/services/secrets"
	"github.com/grafana/grafana/pkg/setting"
)

var (
	fatalFlagOnce             sync.Once
	startupOnce               sync.Once
	errPluginDisabledByConfig = errors.New("remote secret management plugin disabled because the property `secrets.use_plugin` is not set to `true`")
	errPluginNotInstalled     = errors.New("remote secret management plugin disabled because there is no installed plugin of type `secretsmanager`")
)

// SecretsKVStorePlugin provides a key/value store backed by the Grafana plugin gRPC interface
type SecretsKVStorePlugin struct {
	sync.Mutex
	log                            log.Logger
	secretsPlugin                  smp.SecretsManagerPlugin
	secretsService                 secrets.Service
	kvstore                        *kvstore.NamespacedKVStore
	backwardsCompatibilityDisabled bool
	fallbackEnabled                bool
	fallbackStore                  SecretsKVStore
}

func NewPluginSecretsKVStore(
	secretsPlugin smp.SecretsManagerPlugin,
	secretsService secrets.Service,
	kvstore *kvstore.NamespacedKVStore,
	features featuremgmt.FeatureToggles,
	fallback SecretsKVStore,
	logger log.Logger,
) *SecretsKVStorePlugin {
	return &SecretsKVStorePlugin{
		secretsPlugin:                  secretsPlugin,
		secretsService:                 secretsService,
		log:                            logger,
		kvstore:                        kvstore,
		backwardsCompatibilityDisabled: features.IsEnabled(featuremgmt.FlagDisableSecretsCompatibility),
		fallbackStore:                  fallback,
	}
}

// Get an item from the store
// If it is the first time a secret has been retrieved and backwards compatibility is disabled, mark plugin startup errors fatal
func (kv *SecretsKVStorePlugin) Get(ctx context.Context, orgId int64, namespace string, typ string) (string, bool, error) {
	req := &smp.GetSecretRequest{
		KeyDescriptor: &smp.Key{
			OrgId:     orgId,
			Namespace: namespace,
			Type:      typ,
		},
	}

	res, err := kv.secretsPlugin.GetSecret(ctx, req)
	if res.UserFriendlyError != "" {
		err = wrapUserFriendlySecretError(res.UserFriendlyError)
	}

	if res.Exists {
		updateFatalFlag(ctx, kv)
	}

	if kv.fallbackEnabled {
		if err != nil || res.UserFriendlyError != "" || !res.Exists {
			res.DecryptedValue, res.Exists, err = kv.fallbackStore.Get(ctx, orgId, namespace, typ)
		}
	}

	return res.DecryptedValue, res.Exists, err
}

// Set an item in the store
// If it is the first time a secret has been set and backwards compatibility is disabled, mark plugin startup errors fatal
func (kv *SecretsKVStorePlugin) Set(ctx context.Context, orgId int64, namespace string, typ string, value string) error {
	req := &smp.SetSecretRequest{
		KeyDescriptor: &smp.Key{
			OrgId:     orgId,
			Namespace: namespace,
			Type:      typ,
		},
		Value: value,
	}

	res, err := kv.secretsPlugin.SetSecret(ctx, req)
	if err == nil && res.UserFriendlyError != "" {
		err = wrapUserFriendlySecretError(res.UserFriendlyError)
	}

	updateFatalFlag(ctx, kv)

	return err
}

// Del deletes an item from the store.
func (kv *SecretsKVStorePlugin) Del(ctx context.Context, orgId int64, namespace string, typ string) error {
	req := &smp.DeleteSecretRequest{
		KeyDescriptor: &smp.Key{
			OrgId:     orgId,
			Namespace: namespace,
			Type:      typ,
		},
	}

	res, err := kv.secretsPlugin.DeleteSecret(ctx, req)
	if err == nil && res.UserFriendlyError != "" {
		err = wrapUserFriendlySecretError(res.UserFriendlyError)
	}

	return err
}

// Keys get all keys for a given namespace. To query for all
// organizations the constant 'kvstore.AllOrganizations' can be passed as orgId.
func (kv *SecretsKVStorePlugin) Keys(ctx context.Context, orgId int64, namespace string, typ string) ([]Key, error) {
	req := &smp.ListSecretsRequest{
		KeyDescriptor: &smp.Key{
			OrgId:     orgId,
			Namespace: namespace,
			Type:      typ,
		},
		AllOrganizations: orgId == AllOrganizations,
	}

	res, err := kv.secretsPlugin.ListSecrets(ctx, req)
	if err != nil {
		return nil, err
	} else if res.UserFriendlyError != "" {
		err = wrapUserFriendlySecretError(res.UserFriendlyError)
	}

	return parseKeys(res.Keys), err
}

// Rename an item in the store
func (kv *SecretsKVStorePlugin) Rename(ctx context.Context, orgId int64, namespace string, typ string, newNamespace string) error {
	req := &smp.RenameSecretRequest{
		KeyDescriptor: &smp.Key{
			OrgId:     orgId,
			Namespace: namespace,
			Type:      typ,
		},
		NewNamespace: newNamespace,
	}

	res, err := kv.secretsPlugin.RenameSecret(ctx, req)
	if err == nil && res.UserFriendlyError != "" {
		err = wrapUserFriendlySecretError(res.UserFriendlyError)
	}

	return err
}

func (kv *SecretsKVStorePlugin) GetAll(ctx context.Context) ([]Item, error) {
	req := &smp.GetAllSecretsRequest{}

	res, err := kv.secretsPlugin.GetAllSecrets(ctx, req)
	if err != nil {
		return nil, err
	} else if res.UserFriendlyError != "" {
		err = wrapUserFriendlySecretError(res.UserFriendlyError)
	}

	return parseItems(res.Items), err
}

func (kv *SecretsKVStorePlugin) Fallback() SecretsKVStore {
	return kv.fallbackStore
}

func (kv *SecretsKVStorePlugin) WithFallbackEnabled(fn func() error) error {
	kv.Lock()
	defer kv.Unlock()
	kv.fallbackEnabled = true
	err := fn()
	kv.fallbackEnabled = false
	return err
}

func parseKeys(keys []*smp.Key) []Key {
	var newKeys []Key

	for _, k := range keys {
		newKey := Key{OrgId: k.OrgId, Namespace: k.Namespace, Type: k.Type}
		newKeys = append(newKeys, newKey)
	}

	return newKeys
}

func parseItems(items []*smp.Item) []Item {
	var newItems []Item

	for _, i := range items {
		newItem := Item{OrgId: &i.Key.OrgId, Namespace: &i.Key.Namespace, Type: &i.Key.Type, Value: i.Value}
		newItems = append(newItems, newItem)
	}

	return newItems
}

func updateFatalFlag(ctx context.Context, skv *SecretsKVStorePlugin) {
	// This function makes the most sense in here because it handles all possible scenarios:
	//   - User changed backwards compatibility flag, so we have to migrate secrets either to or from the plugin (get or set)
	//   - Migration is on, so we migrate secrets to the plugin (set)
	//   - User doesn't migrate, but stores a new secret in the plugin (set)
	// Rather than updating the flag in several places, it is cleaner to just do this check once
	// Very early on. Once backwards compatibility to legacy secrets is gone in Grafana 10, this can go away as well
	fatalFlagOnce.Do(func() {
		skv.log.Debug("Updating plugin startup error fatal flag")
		var err error
		if isFatal, _ := IsPluginStartupErrorFatal(ctx, skv.kvstore); !isFatal && skv.backwardsCompatibilityDisabled {
			err = SetPluginStartupErrorFatal(ctx, skv.kvstore, true)
		} else if isFatal && !skv.backwardsCompatibilityDisabled {
			err = SetPluginStartupErrorFatal(ctx, skv.kvstore, false)
		}
		if err != nil {
			skv.log.Error("failed to set plugin error fatal flag", err.Error())
		}
	})
}

func wrapUserFriendlySecretError(ufe string) datasources.ErrDatasourceSecretsPluginUserFriendly {
	return datasources.ErrDatasourceSecretsPluginUserFriendly{Err: ufe}
}

func GetNamespacedKVStore(kv kvstore.KVStore) *kvstore.NamespacedKVStore {
	return kvstore.WithNamespace(kv, kvstore.AllOrganizations, PluginNamespace)
}

func IsPluginStartupErrorFatal(ctx context.Context, kvstore *kvstore.NamespacedKVStore) (bool, error) {
	_, exists, err := kvstore.Get(ctx, QuitOnPluginStartupFailureKey)
	if err != nil {
		return false, errors.New(fmt.Sprint("error retrieving key ", QuitOnPluginStartupFailureKey, " from kvstore. error: ", err.Error()))
	}
	return exists, nil
}

func SetPluginStartupErrorFatal(ctx context.Context, kvstore *kvstore.NamespacedKVStore, isFatal bool) error {
	if !isFatal {
		return kvstore.Del(ctx, QuitOnPluginStartupFailureKey)
	}
	return kvstore.Set(ctx, QuitOnPluginStartupFailureKey, "true")
}

func EvaluateRemoteSecretsPlugin(ctx context.Context, mg plugins.SecretsPluginManager, cfg *setting.Cfg) error {
	usePlugin := cfg.SectionWithEnvOverrides("secrets").Key("use_plugin").MustBool()
	if !usePlugin {
		return errPluginDisabledByConfig
	}
	pluginInstalled := mg.SecretsManager(ctx) != nil
	if !pluginInstalled {
		return errPluginNotInstalled
	}
	return nil
}

func HasPluginStarted(ctx context.Context, mg plugins.SecretsPluginManager) bool {
	return mg.SecretsManager(ctx) != nil && mg.SecretsManager(ctx).SecretsManager != nil
}

func StartAndReturnPlugin(mg plugins.SecretsPluginManager, ctx context.Context) (smp.SecretsManagerPlugin, error) {
	var err error
	startupOnce.Do(func() {
		err = mg.SecretsManager(ctx).Start(ctx)
	})
	if err != nil {
		return nil, err
	}
	return mg.SecretsManager(ctx).SecretsManager, nil
}

func ResetPlugin() {
	fatalFlagOnce = sync.Once{}
	startupOnce = sync.Once{}
}
