package initialization

import (
	"context"
	"errors"

	"github.com/grafana/grafana/pkg/plugins"
	"github.com/grafana/grafana/pkg/plugins/envvars"
	"github.com/grafana/grafana/pkg/plugins/log"
	"github.com/grafana/grafana/pkg/plugins/manager/process"
	"github.com/grafana/grafana/pkg/plugins/manager/registry"
	"go.opentelemetry.io/otel/trace"
)

// BackendClientInit implements an InitializeFunc for initializing a backend plugin process.
//
// It uses the envvars.Provider to retrieve the environment variables required for the plugin and the plugins.BackendFactoryProvider
// to get fetch backend factory, which is used to form a connection to the backend plugin process.
//
// Note: This step does not start the backend plugin process. Please see BackendClientStarter for starting the backend plugin process.
type BackendClientInit struct {
	envVarProvider  envvars.Provider
	backendProvider plugins.BackendFactoryProvider
	log             log.Logger
	tracer          trace.Tracer
}

// BackendClientInitStep returns a new InitializeFunc for registering a backend plugin process.
func BackendClientInitStep(envVarProvider envvars.Provider,
	backendProvider plugins.BackendFactoryProvider, tracer trace.Tracer) InitializeFunc {
	return newBackendProcessRegistration(envVarProvider, backendProvider, tracer).Initialize
}

func newBackendProcessRegistration(envVarProvider envvars.Provider,
	backendProvider plugins.BackendFactoryProvider, tracer trace.Tracer) *BackendClientInit {
	return &BackendClientInit{
		backendProvider: backendProvider,
		envVarProvider:  envVarProvider,
		log:             log.New("plugins.backend.registration"),
		tracer:          tracer,
	}
}

// Initialize will initialize a backend plugin client, if the plugin is a backend plugin.
func (b *BackendClientInit) Initialize(ctx context.Context, p *plugins.Plugin) (*plugins.Plugin, error) {
	if p.Backend {
		backendFactory := b.backendProvider.BackendFactory(ctx, p)
		if backendFactory == nil {
			return nil, errors.New("could not find backend factory for plugin")
		}

		// this will ensure that the env variables are calculated every time a plugin is started
		envFunc := func() []string { return b.envVarProvider.PluginEnvVars(ctx, p) }

		if backendClient, err := backendFactory(p.ID, p.Logger(), b.tracer, envFunc); err != nil {
			return nil, err
		} else {
			p.RegisterClient(backendClient)
		}
	}
	return p, nil
}

// BackendClientStarter implements an InitializeFunc for starting a backend plugin process.
type BackendClientStarter struct {
	processManager process.Manager
	log            log.Logger
}

// BackendProcessStartStep returns a new InitializeFunc for starting a backend plugin process.
func BackendProcessStartStep(processManager process.Manager) InitializeFunc {
	return newBackendProcessStarter(processManager).Start
}

func newBackendProcessStarter(processManager process.Manager) *BackendClientStarter {
	return &BackendClientStarter{
		processManager: processManager,
		log:            log.New("plugins.backend.start"),
	}
}

// Start will start the backend plugin process.
func (b *BackendClientStarter) Start(ctx context.Context, p *plugins.Plugin) (*plugins.Plugin, error) {
	if err := b.processManager.Start(ctx, p); err != nil {
		b.log.Error("Could not start plugin backend", "pluginId", p.ID, "error", err)
		return nil, (&plugins.Error{
			PluginID:  p.ID,
			ErrorCode: plugins.ErrorCodeFailedBackendStart,
		}).WithMessage(err.Error())
	}
	return p, nil
}

// PluginRegistration implements an InitializeFunc for registering a plugin with the plugin registry.
type PluginRegistration struct {
	pluginRegistry registry.Service
	log            log.Logger
}

// PluginRegistrationStep returns a new InitializeFunc for registering a plugin with the plugin registry.
func PluginRegistrationStep(pluginRegistry registry.Service) InitializeFunc {
	return newPluginRegistration(pluginRegistry).Initialize
}

func newPluginRegistration(pluginRegistry registry.Service) *PluginRegistration {
	return &PluginRegistration{
		pluginRegistry: pluginRegistry,
		log:            log.New("plugins.registration"),
	}
}

// Initialize registers the plugin with the plugin registry.
func (r *PluginRegistration) Initialize(ctx context.Context, p *plugins.Plugin) (*plugins.Plugin, error) {
	if err := r.pluginRegistry.Add(ctx, p); err != nil {
		r.log.Error("Could not register plugin", "pluginId", p.ID, "error", err)
		return nil, err
	}
	if !p.IsCorePlugin() {
		r.log.Info("Plugin registered", "pluginId", p.ID)
	}

	return p, nil
}
