package pluginstore

import (
	"context"
	"sort"
	"time"

	"github.com/grafana/dskit/services"
	"golang.org/x/sync/errgroup"

	"github.com/grafana/grafana/pkg/infra/log"
	"github.com/grafana/grafana/pkg/plugins"
	"github.com/grafana/grafana/pkg/plugins/manager/loader"
	"github.com/grafana/grafana/pkg/plugins/manager/registry"
	"github.com/grafana/grafana/pkg/plugins/manager/sources"
	"github.com/grafana/grafana/pkg/services/featuremgmt"
)

var _ Store = (*Service)(nil)

const ServiceName = "plugins.store"

// Store is the publicly accessible storage for plugins.
type Store interface {
	// Plugin finds a plugin by its ID.
	// Note: version is not required since Grafana only supports single versions of a plugin.
	Plugin(ctx context.Context, pluginID string) (Plugin, bool)
	// Plugins returns plugins by their requested type.
	Plugins(ctx context.Context, pluginTypes ...plugins.Type) []Plugin
}

type Service struct {
	services.NamedService

	pluginRegistry registry.Service
	pluginLoader   loader.Service
	pluginSources  sources.Registry
	loadOnStartup  bool
}

func ProvideService(pluginRegistry registry.Service, pluginSources sources.Registry,
	pluginLoader loader.Service, features featuremgmt.FeatureToggles) (*Service, error) {
	//nolint:staticcheck // not yet migrated to OpenFeature
	if features.IsEnabledGlobally(featuremgmt.FlagPluginStoreServiceLoading) {
		s := New(pluginRegistry, pluginLoader, pluginSources)
		s.loadOnStartup = true
		return s, nil
	}

	ctx := context.Background()
	start := time.Now()
	totalPlugins := 0
	logger := log.New("plugin.store")
	logger.Info("Loading plugins...")

	for _, ps := range pluginSources.List(ctx) {
		loadedPlugins, err := pluginLoader.Load(ctx, ps)
		if err != nil {
			logger.Error("Loading plugin source failed", "source", ps.PluginClass(ctx), "error", err)
			return nil, err
		}
		totalPlugins += len(loadedPlugins)
	}

	logger.Info("Plugins loaded", "count", totalPlugins, "duration", time.Since(start))

	return New(pluginRegistry, pluginLoader, pluginSources), nil
}

func (s *Service) Run(ctx context.Context) error {
	if err := s.StartAsync(ctx); err != nil {
		return err
	}
	stopCtx := context.Background()
	return s.AwaitTerminated(stopCtx)
}

func NewPluginStoreForTest(pluginRegistry registry.Service, pluginLoader loader.Service, pluginSources sources.Registry) (*Service, error) {
	s := New(pluginRegistry, pluginLoader, pluginSources)
	s.loadOnStartup = true
	if err := s.StartAsync(context.Background()); err != nil {
		return nil, err
	}
	if err := s.AwaitRunning(context.Background()); err != nil {
		return nil, err
	}
	return s, nil
}

func New(pluginRegistry registry.Service, pluginLoader loader.Service, pluginSources sources.Registry) *Service {
	s := &Service{
		pluginRegistry: pluginRegistry,
		pluginLoader:   pluginLoader,
		pluginSources:  pluginSources,
	}
	s.NamedService = services.NewBasicService(s.starting, s.running, s.stopping).WithName(ServiceName)
	return s
}

func (s *Service) starting(ctx context.Context) error {
	if !s.loadOnStartup {
		return nil
	}
	start := time.Now()
	totalPlugins := 0
	logger := log.New(ServiceName)
	logger.Info("Loading plugins...")

	for _, ps := range s.pluginSources.List(ctx) {
		loadedPlugins, err := s.pluginLoader.Load(ctx, ps)
		if err != nil {
			logger.Error("Loading plugin source failed", "source", ps.PluginClass(ctx), "error", err)
			return err
		}
		totalPlugins += len(loadedPlugins)
	}

	logger.Info("Plugins loaded", "count", totalPlugins, "duration", time.Since(start))
	return nil
}

func (s *Service) running(ctx context.Context) error {
	<-ctx.Done()
	return nil
}

func (s *Service) stopping(failureReason error) error {
	return s.shutdown(context.Background())
}

func (s *Service) Plugin(ctx context.Context, pluginID string) (Plugin, bool) {
	if err := s.AwaitRunning(ctx); err != nil {
		log.New(ServiceName).FromContext(ctx).Error("Failed to get plugin", "error", err)
		return Plugin{}, false
	}
	p, exists := s.plugin(ctx, pluginID)
	if !exists {
		return Plugin{}, false
	}

	return ToGrafanaDTO(p), true
}

func (s *Service) Plugins(ctx context.Context, pluginTypes ...plugins.Type) []Plugin {
	if err := s.AwaitRunning(ctx); err != nil {
		log.New(ServiceName).FromContext(ctx).Error("Failed to get plugins", "error", err)
		return []Plugin{}
	}
	// if no types passed, assume all
	if len(pluginTypes) == 0 {
		pluginTypes = plugins.PluginTypes
	}

	var requestedTypes = make(map[plugins.Type]struct{})
	for _, pt := range pluginTypes {
		requestedTypes[pt] = struct{}{}
	}

	pluginsList := make([]Plugin, 0)
	for _, p := range s.availablePlugins(ctx) {
		if _, exists := requestedTypes[p.Type]; exists {
			pluginsList = append(pluginsList, ToGrafanaDTO(p))
		}
	}
	return pluginsList
}

// plugin finds a plugin with `pluginID` from the registry that is not decommissioned
func (s *Service) plugin(ctx context.Context, pluginID string) (*plugins.Plugin, bool) {
	p, exists := s.pluginRegistry.Plugin(ctx, pluginID, "") // version is not required since Grafana only supports single versions of a plugin
	if !exists {
		return nil, false
	}

	if p.IsDecommissioned() {
		return nil, false
	}

	return p, true
}

// availablePlugins returns all non-decommissioned plugins from the registry sorted by alphabetic order on `plugin.ID`
func (s *Service) availablePlugins(ctx context.Context) []*plugins.Plugin {
	ps := s.pluginRegistry.Plugins(ctx)

	res := make([]*plugins.Plugin, 0, len(ps))
	for _, p := range ps {
		if !p.IsDecommissioned() {
			res = append(res, p)
		}
	}
	sort.SliceStable(res, func(i, j int) bool {
		return res[i].ID < res[j].ID
	})
	return res
}

func (s *Service) Routes(ctx context.Context) []*plugins.StaticRoute {
	if err := s.AwaitRunning(ctx); err != nil {
		log.New(ServiceName).FromContext(ctx).Error("Failed to get routes", "error", err)
		return []*plugins.StaticRoute{}
	}
	staticRoutes := make([]*plugins.StaticRoute, 0)

	for _, p := range s.availablePlugins(ctx) {
		if p.StaticRoute() != nil {
			staticRoutes = append(staticRoutes, p.StaticRoute())
		}
	}
	return staticRoutes
}

func (s *Service) shutdown(ctx context.Context) error {
	var errgroup errgroup.Group
	plugins := s.pluginRegistry.Plugins(ctx)
	for _, p := range plugins {
		plugin := p // capture loop variable
		errgroup.Go(func() error {
			plugin.Logger().Debug("Stopping plugin")
			if _, err := s.pluginLoader.Unload(ctx, plugin); err != nil {
				plugin.Logger().Error("Failed to stop plugin", "error", err)
				return err
			}
			plugin.Logger().Debug("Plugin stopped")
			return nil
		})
	}
	return errgroup.Wait()
}
