package registry

// FIXME (gamab): we can eventually remove this package

import (
	"context"
	"sync"
	"time"

	"github.com/grafana/grafana/pkg/infra/log"
	"github.com/grafana/grafana/pkg/infra/serverlock"
	"github.com/grafana/grafana/pkg/infra/slugify"
	"github.com/grafana/grafana/pkg/services/extsvcauth"
	"github.com/grafana/grafana/pkg/services/featuremgmt"
	"github.com/grafana/grafana/pkg/services/serviceaccounts/extsvcaccounts"
	"github.com/grafana/grafana/pkg/setting"
)

var _ extsvcauth.ExternalServiceRegistry = &Registry{}

var lockTimeConfig = serverlock.LockTimeConfig{
	MaxInterval: 2 * time.Minute,
	MinWait:     1 * time.Second,
	MaxWait:     5 * time.Second,
}

type serverLocker interface {
	LockExecuteAndReleaseWithRetries(context.Context, string, serverlock.LockTimeConfig, func(ctx context.Context), ...serverlock.RetryOpt) error
}

type Registry struct {
	enabled bool
	logger  log.Logger
	saReg   extsvcauth.ExternalServiceRegistry

	// FIXME (gamab): we can remove this field and use the saReg.GetExternalServiceNames directly
	extSvcProviders map[string]extsvcauth.AuthProvider
	lock            sync.Mutex
	serverLock      serverLocker
}

func ProvideExtSvcRegistry(cfg *setting.Cfg, saSvc *extsvcaccounts.ExtSvcAccountsService, serverLock *serverlock.ServerLockService, features featuremgmt.FeatureToggles) *Registry {
	enabled := features.IsEnabledGlobally(featuremgmt.FlagExternalServiceAccounts) && cfg.ManagedServiceAccountsEnabled
	return &Registry{
		extSvcProviders: map[string]extsvcauth.AuthProvider{},
		enabled:         enabled,
		lock:            sync.Mutex{},
		logger:          log.New("extsvcauth.registry"),
		saReg:           saSvc,
		serverLock:      serverLock,
	}
}

// CleanUpOrphanedExternalServices remove external services present in store that have not been registered on startup.
func (r *Registry) CleanUpOrphanedExternalServices(ctx context.Context) error {
	var errCleanUp error

	errLock := r.serverLock.LockExecuteAndReleaseWithRetries(ctx, "ext-svc-clean-up", lockTimeConfig, func(ctx context.Context) {
		extsvcs, err := r.retrieveExtSvcProviders(ctx)
		if err != nil {
			r.logger.Error("Could not retrieve external services from store", "error", err.Error())
			errCleanUp = err
			return
		}
		for name, provider := range extsvcs {
			// The service did not register this time. Removed.
			if _, ok := r.extSvcProviders[slugify.Slugify(name)]; !ok {
				r.logger.Info("Detected removed External Service", "service", name, "provider", provider)
				switch provider {
				case extsvcauth.ServiceAccounts:
					if err := r.saReg.RemoveExternalService(ctx, name); err != nil {
						errCleanUp = err
						return
					}
				}
			}
		}
	})
	if errLock != nil {
		return errLock
	}

	return errCleanUp
}

// HasExternalService returns whether an external service has been saved with that name.
func (r *Registry) HasExternalService(ctx context.Context, name string) (bool, error) {
	_, ok := r.extSvcProviders[slugify.Slugify(name)]
	return ok, nil
}

// GetExternalServiceNames returns the list of external services registered in store.
func (r *Registry) GetExternalServiceNames(ctx context.Context) ([]string, error) {
	extSvcProviders, err := r.retrieveExtSvcProviders(ctx)
	if err != nil {
		return nil, err
	}
	names := []string{}
	for s := range extSvcProviders {
		names = append(names, s)
	}
	return names, nil
}

// RemoveExternalService removes an external service and its associated resources from the database (ex: service account, token).
func (r *Registry) RemoveExternalService(ctx context.Context, name string) error {
	provider, ok := r.extSvcProviders[slugify.Slugify(name)]
	if !ok {
		r.logger.Debug("external service not found", "service", name)
		return nil
	}

	switch provider {
	case extsvcauth.ServiceAccounts:
		if !r.enabled {
			r.logger.Warn("Skipping External Service authentication, flag or configuration option is disabled",
				"service", name,
				"flag", featuremgmt.FlagExternalServiceAccounts,
				"option", "ManagedServiceAccountsEnabled",
			)
			return nil
		}
		r.logger.Debug("Routing External Service removal to the External Service Account service", "service", name)
		return r.saReg.RemoveExternalService(ctx, name)
	default:
		return extsvcauth.ErrUnknownProvider.Errorf("unknown provider '%v'", provider)
	}
}

// SaveExternalService creates or updates an external service in the database. Based on the requested auth provider,
// it generates client_id, secrets and any additional provider specificities (ex: rsa keys). It also ensures that the
// associated service account has the correct permissions.
func (r *Registry) SaveExternalService(ctx context.Context, cmd *extsvcauth.ExternalServiceRegistration) (*extsvcauth.ExternalService, error) {
	var (
		errSave   error
		extSvc    *extsvcauth.ExternalService
		lockName  = "ext-svc-save-" + cmd.Name
		ctxLogger = r.logger.FromContext(ctx)
	)

	err := r.serverLock.LockExecuteAndReleaseWithRetries(ctx, lockName, lockTimeConfig, func(ctx context.Context) {
		// Record provider in case of removal
		r.lock.Lock()
		r.extSvcProviders[slugify.Slugify(cmd.Name)] = cmd.AuthProvider
		r.lock.Unlock()

		switch cmd.AuthProvider {
		case extsvcauth.ServiceAccounts:
			if !r.enabled {
				ctxLogger.Warn("Skipping External Service authentication, flag or configuration option disabled",
					"service", cmd.Name,
					"flag", featuremgmt.FlagExternalServiceAccounts,
					"option", "ManagedServiceAccountsEnabled",
				)
				return
			}
			ctxLogger.Debug("Routing the External Service registration to the External Service Account service", "service", cmd.Name)
			extSvc, errSave = r.saReg.SaveExternalService(ctx, cmd)
		default:
			errSave = extsvcauth.ErrUnknownProvider.Errorf("unknown provider '%v'", cmd.AuthProvider)
		}
	})
	if err != nil {
		return nil, err
	}

	return extSvc, errSave
}

// retrieveExtSvcProviders fetches external services from store and map their associated provider
func (r *Registry) retrieveExtSvcProviders(ctx context.Context) (map[string]extsvcauth.AuthProvider, error) {
	extsvcs := map[string]extsvcauth.AuthProvider{}
	if r.enabled {
		names, err := r.saReg.GetExternalServiceNames(ctx)
		if err != nil {
			return nil, err
		}
		for i := range names {
			extsvcs[names[i]] = extsvcauth.ServiceAccounts
		}
	}

	return extsvcs, nil
}

// func (r *Registry) Run(ctx context.Context) error {
// 	// This is a one-time background job.
// 	// Cleans up external services that have not been registered this time.
// 	return r.CleanUpOrphanedExternalServices(ctx)
// }
