package supportbundlesimpl

import (
	"context"
	"encoding/json"
	"fmt"
	"runtime"
	"time"

	"github.com/grafana/grafana/pkg/infra/log"
	"github.com/grafana/grafana/pkg/plugins"
	"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/setting"
)

func basicCollector(cfg *setting.Cfg) supportbundles.Collector {
	return supportbundles.Collector{
		UID:               "basic",
		DisplayName:       "Basic information",
		Description:       "Basic information about the Grafana instance",
		IncludedByDefault: true,
		Default:           true,
		Fn: func(ctx context.Context) (*supportbundles.SupportItem, error) {
			type basicInfo struct {
				Version         string    `json:"version"`          // Version is the version of Grafana instance.
				Commit          string    `json:"commit"`           // Commit is the commit hash of the Grafana instance.
				CollectionDate  time.Time `json:"collection_date"`  // CollectionDate is the date when the support bundle was created.
				DefaultTimezone string    `json:"default_timezone"` // DefaultTimezone is the default timezone of the Grafana instance.
				Alloc           uint64    `json:"alloc"`            // Alloc is bytes of allocated heap objects.
				TotalAlloc      uint64    `json:"total_alloc"`      // TotalAlloc is cumulative bytes allocated for heap objects.
				Sys             uint64    `json:"sys"`              // Sys is the total bytes of memory obtained from the OS.
				Mallocs         uint64    `json:"mallocs"`          // Mallocs is the cumulative count of heap objects allocated.
				Frees           uint64    `json:"frees"`            // Frees is the cumulative count of heap objects freed.
				NumGC           uint32    `json:"num_gc"`           // NumGC is the number of completed GC cycles.
				PauseTotalNs    uint64    `json:"pause_total_ns"`   // PauseTotalNs is the cumulative nanoseconds in GC
				NumCPU          int       `json:"num_cpu"`          // NumCPU is the number of logical CPUs usable by the current process.
				NumGoRoutines   int       `json:"num_go_routines"`  // NumGoRoutines is the number of goroutines that currently exist.
				GoVersion       string    `json:"go_version"`       // GoVersion is the version of Go used to build the binary.
				GoOS            string    `json:"go_os"`            // GoOS is the operating system target used to build the binary.
				GoArch          string    `json:"go_arch"`          // GoArch is the architecture target used to build the binary.
				GoCompiler      string    `json:"go_compiler"`      // GoCompiler is the compiler used to build the binary.
			}

			memstats := runtime.MemStats{}
			runtime.ReadMemStats(&memstats)

			collectionDate := time.Now()
			tz, offset := collectionDate.Zone()

			loc, _ := time.LoadLocation("UTC")
			now := collectionDate.In(loc)

			info := basicInfo{
				Version:         cfg.BuildVersion,
				Commit:          cfg.BuildCommit,
				CollectionDate:  now,
				DefaultTimezone: fmt.Sprintf("%s (UTC%+d)", tz, offset/60/60),
				Alloc:           memstats.Alloc,
				TotalAlloc:      memstats.TotalAlloc,
				Sys:             memstats.Sys,
				Mallocs:         memstats.Mallocs,
				Frees:           memstats.Frees,
				NumGC:           memstats.NumGC,
				PauseTotalNs:    memstats.PauseTotalNs,
				NumCPU:          runtime.NumCPU(),
				NumGoRoutines:   runtime.NumGoroutine(),
				GoVersion:       runtime.Version(),
				GoOS:            runtime.GOOS,
				GoArch:          runtime.GOARCH,
				GoCompiler:      runtime.Compiler,
			}
			data, err := json.MarshalIndent(info, "", " ")
			if err != nil {
				return nil, err
			}

			return &supportbundles.SupportItem{
				Filename:  "basic.json",
				FileBytes: data,
			}, nil
		},
	}
}

func settingsCollector(settings setting.Provider) supportbundles.Collector {
	return supportbundles.Collector{
		UID:               "settings",
		DisplayName:       "Settings",
		Description:       "Settings of the Grafana instance",
		IncludedByDefault: false,
		Default:           true,
		Fn: func(ctx context.Context) (*supportbundles.SupportItem, error) {
			current := settings.Current()
			data, err := json.MarshalIndent(current, "", " ")
			if err != nil {
				return nil, err
			}

			return &supportbundles.SupportItem{
				Filename:  "settings.json",
				FileBytes: data,
			}, nil
		},
	}
}

func pluginInfoCollector(pluginStore pluginstore.Store, pluginSettings pluginsettings.Service, logger log.Logger) supportbundles.Collector {
	return supportbundles.Collector{
		UID:               "plugins",
		DisplayName:       "Plugin information",
		Description:       "Plugin information for the Grafana instance",
		IncludedByDefault: false,
		Default:           true,
		Fn: func(ctx context.Context) (*supportbundles.SupportItem, error) {
			type PluginInfo struct {
				id          string
				Name        string
				Description string
				PluginType  string `json:"type"`

				Author          plugins.InfoLink `json:"author,omitempty"`
				SignatureStatus plugins.SignatureStatus
				SignatureType   plugins.SignatureType
				SignatureOrg    string

				GrafanaVersionDependency string `json:"grafanaVersionDependency,omitempty"`

				Settings []pluginsettings.InfoDTO `json:"settings,omitempty"`
			}

			// plugin definitions
			plugins := pluginStore.Plugins(ctx)

			// plugin settings
			settings, err := pluginSettings.GetPluginSettings(ctx, &pluginsettings.GetArgs{})
			if err != nil {
				logger.Debug("Failed to fetch plugin settings:", "err", err)
			}

			settingMap := make(map[string][]*pluginsettings.InfoDTO)
			for _, ps := range settings {
				settingMap[ps.PluginID] = append(settingMap[ps.PluginID], ps)
			}

			var pluginInfoList []PluginInfo
			for _, plugin := range plugins {
				// skip builtin and core plugins
				if plugin.BuiltIn || plugin.IsCorePlugin() {
					continue
				}
				// skip plugins that are included in another plugin
				if plugin.IncludedInAppID != "" {
					continue
				}

				pInfo := PluginInfo{
					id:                       plugin.ID,
					Name:                     plugin.Name,
					Description:              plugin.Info.Description,
					PluginType:               string(plugin.Type),
					Author:                   plugin.Info.Author,
					SignatureStatus:          plugin.Signature,
					SignatureType:            plugin.SignatureType,
					SignatureOrg:             plugin.SignatureOrg,
					GrafanaVersionDependency: plugin.Dependencies.GrafanaVersion,
				}

				for _, ps := range settingMap[plugin.ID] {
					pInfo.Settings = append(pInfo.Settings, pluginsettings.InfoDTO{
						OrgID:         ps.OrgID,
						Enabled:       ps.Enabled,
						PluginVersion: ps.PluginVersion,
					})
				}

				pluginInfoList = append(pluginInfoList, pInfo)
			}

			data, err := json.MarshalIndent(pluginInfoList, "", " ")
			if err != nil {
				return nil, err
			}
			return &supportbundles.SupportItem{
				Filename:  "plugins.json",
				FileBytes: data,
			}, nil
		},
	}
}
