package supportbundlesimpl

import (
	"context"
	"fmt"
	"time"

	grafanaApi "github.com/grafana/grafana/pkg/api"
	"github.com/grafana/grafana/pkg/api/routing"
	"github.com/grafana/grafana/pkg/apimachinery/identity"
	"github.com/grafana/grafana/pkg/infra/db"
	"github.com/grafana/grafana/pkg/infra/kvstore"
	"github.com/grafana/grafana/pkg/infra/log"
	"github.com/grafana/grafana/pkg/infra/tracing"
	"github.com/grafana/grafana/pkg/infra/usagestats"
	ac "github.com/grafana/grafana/pkg/services/accesscontrol"
	"github.com/grafana/grafana/pkg/services/featuremgmt"
	"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginsettings"
	"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore"
	"github.com/grafana/grafana/pkg/services/supportbundles"
	"github.com/grafana/grafana/pkg/services/supportbundles/bundleregistry"
	"github.com/grafana/grafana/pkg/setting"
)

const (
	cleanUpInterval       = 24 * time.Hour
	bundleCreationTimeout = 20 * time.Minute
)

type Service struct {
	accessControl  ac.AccessControl
	bundleRegistry *bundleregistry.Service
	cfg            *setting.Cfg
	features       featuremgmt.FeatureToggles
	pluginSettings pluginsettings.Service
	pluginStore    pluginstore.Store
	store          bundleStore
	tracer         tracing.Tracer

	log                  log.Logger
	encryptionPublicKeys []string

	enabled         bool
	serverAdminOnly bool
}

func ProvideService(
	accessControl ac.AccessControl,
	accesscontrolService ac.Service,
	bundleRegistry *bundleregistry.Service,
	cfg *setting.Cfg,
	features featuremgmt.FeatureToggles,
	httpServer *grafanaApi.HTTPServer,
	kvStore kvstore.KVStore,
	pluginSettings pluginsettings.Service,
	pluginStore pluginstore.Store,
	routeRegister routing.RouteRegister,
	settings setting.Provider,
	sql db.DB,
	usageStats usagestats.Service,
	tracer tracing.Tracer) (*Service, error) {
	section := cfg.SectionWithEnvOverrides("support_bundles")
	s := &Service{
		accessControl:        accessControl,
		bundleRegistry:       bundleRegistry,
		cfg:                  cfg,
		enabled:              section.Key("enabled").MustBool(true),
		encryptionPublicKeys: section.Key("public_keys").Strings(" "),
		features:             features,
		log:                  log.New("supportbundle.service"),
		pluginSettings:       pluginSettings,
		pluginStore:          pluginStore,
		serverAdminOnly:      section.Key("server_admin_only").MustBool(true),
		store:                newStore(kvStore),
		tracer:               tracer,
	}

	usageStats.RegisterMetricsFunc(s.getUsageStats)

	if !s.enabled {
		return s, nil
	}

	if err := s.declareFixedRoles(accesscontrolService); err != nil {
		return nil, err
	}

	s.registerAPIEndpoints(httpServer, routeRegister)

	// TODO: move to relevant services
	s.bundleRegistry.RegisterSupportItemCollector(basicCollector(cfg))
	s.bundleRegistry.RegisterSupportItemCollector(settingsCollector(settings))
	s.bundleRegistry.RegisterSupportItemCollector(dbCollector(sql))
	s.bundleRegistry.RegisterSupportItemCollector(pluginInfoCollector(pluginStore, pluginSettings, s.log))

	return s, nil
}

func (s *Service) Run(ctx context.Context) error {
	if !s.enabled {
		return nil
	}

	ticker := time.NewTicker(cleanUpInterval)
	defer ticker.Stop()
	s.cleanup(ctx)
	select {
	case <-ticker.C:
		s.cleanup(ctx)
	case <-ctx.Done():
		break
	}
	return ctx.Err()
}

func (s *Service) create(ctx context.Context, collectors []string, usr identity.Requester) (*supportbundles.Bundle, error) {
	bundle, err := s.store.Create(ctx, usr)
	if err != nil {
		return nil, err
	}

	go func(uid string, collectors []string) {
		ctx, cancel := context.WithTimeout(context.Background(), bundleCreationTimeout)
		defer func() {
			if err := recover(); err != nil {
				s.log.Error("Support bundle collection panic", "err", err)
			}
			cancel()
		}()

		s.startBundleWork(ctx, collectors, uid)
	}(bundle.UID, collectors)

	return bundle, nil
}

func (s *Service) get(ctx context.Context, uid string) (*supportbundles.Bundle, error) {
	return s.store.Get(ctx, uid)
}

func (s *Service) list(ctx context.Context) ([]supportbundles.Bundle, error) {
	return s.store.List()
}

func (s *Service) remove(ctx context.Context, uid string) error {
	// Remove the data
	bundle, err := s.store.Get(ctx, uid)
	if err != nil {
		return fmt.Errorf("could not retrieve support bundle with UID %s: %w", uid, err)
	}

	// TODO handle cases when bundles aren't complete yet
	if bundle.State == supportbundles.StatePending {
		return fmt.Errorf("could not remove a support bundle with uid %s as it is still being created", uid)
	}

	// Remove the KV store entry
	return s.store.Remove(ctx, uid)
}

func (s *Service) cleanup(ctx context.Context) {
	bundles, err := s.list(ctx)
	if err != nil {
		s.log.Error("Failed to list bundles to clean up", "error", err)
	}

	if err == nil {
		for _, b := range bundles {
			if time.Now().Unix() >= b.ExpiresAt {
				if err := s.remove(ctx, b.UID); err != nil {
					s.log.Error("Failed to cleanup bundle", "error", err)
				}
			}
		}
	}
}

func (s *Service) getUsageStats(ctx context.Context) (map[string]interface{}, error) {
	m := map[string]interface{}{}

	count, err := s.store.StatsCount(ctx)
	if err != nil {
		s.log.Warn("Unable to get support bundle counter", "error", err)
	}

	m["stats.bundles.count"] = count
	return m, nil
}
