package install

import (
	"context"
	"sync"
	"time"

	"github.com/grafana/grafana-app-sdk/logging"
	"github.com/grafana/grafana-app-sdk/resource"
	errorsK8s "k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

	pluginsv0alpha1 "github.com/grafana/grafana/apps/plugins/pkg/apis/plugins/v0alpha1"
	"github.com/grafana/grafana/apps/plugins/pkg/app/metrics"
)

const (
	PluginInstallSourceAnnotation = "plugins.grafana.app/install-source"
)

type Source = string

const (
	SourceUnknown               Source = "unknown"
	SourcePluginStore           Source = "plugin-store"
	SourceChildPluginReconciler Source = "child-plugin-reconciler"
)

// Registrar is an interface for registering plugin installations.
type Registrar interface {
	Register(ctx context.Context, namespace string, install *PluginInstall) error
	Unregister(ctx context.Context, namespace string, name string, source Source) error
}

type PluginInstall struct {
	ID       string
	Version  string
	URL      string
	Source   Source
	ParentID string
}

func (p *PluginInstall) ToPluginInstallV0Alpha1(namespace string) *pluginsv0alpha1.Plugin {
	var url *string = nil
	if p.URL != "" {
		url = &p.URL
	}
	var parentID *string = nil
	if p.ParentID != "" {
		parentID = &p.ParentID
	}
	return &pluginsv0alpha1.Plugin{
		ObjectMeta: metav1.ObjectMeta{
			Namespace: namespace,
			Name:      p.ID,
			Annotations: map[string]string{
				PluginInstallSourceAnnotation: p.Source,
			},
		},
		Spec: pluginsv0alpha1.PluginSpec{
			Id:       p.ID,
			Version:  p.Version,
			Url:      url,
			ParentId: parentID,
		},
	}
}

func (p *PluginInstall) ShouldUpdate(existing *pluginsv0alpha1.Plugin) bool {
	update := p.ToPluginInstallV0Alpha1(existing.Namespace)
	if source, ok := existing.Annotations[PluginInstallSourceAnnotation]; ok && source != p.Source {
		return true
	}
	if existing.Spec.Version != update.Spec.Version {
		return true
	}
	if !equalStringPointers(existing.Spec.Url, update.Spec.Url) {
		return true
	}
	if !equalStringPointers(existing.Spec.ParentId, update.Spec.ParentId) {
		return true
	}
	return false
}

func equalStringPointers(a, b *string) bool {
	if a == nil && b == nil {
		return true
	}
	if a == nil || b == nil {
		return false
	}
	return *a == *b
}

type InstallRegistrar struct {
	clientGenerator resource.ClientGenerator
	client          *pluginsv0alpha1.PluginClient
	clientErr       error
	clientOnce      sync.Once
	logger          logging.Logger
}

func NewInstallRegistrar(logger logging.Logger, clientGenerator resource.ClientGenerator) *InstallRegistrar {
	return &InstallRegistrar{
		clientGenerator: clientGenerator,
		clientOnce:      sync.Once{},
		logger:          logger,
	}
}

func (r *InstallRegistrar) GetClient() (*pluginsv0alpha1.PluginClient, error) {
	r.clientOnce.Do(func() {
		client, err := pluginsv0alpha1.NewPluginClientFromGenerator(r.clientGenerator)
		if err != nil {
			r.clientErr = err
			r.client = nil
			return
		}
		r.client = client
	})

	return r.client, r.clientErr
}

// Register creates or updates a plugin install in the registry.
func (r *InstallRegistrar) Register(ctx context.Context, namespace string, install *PluginInstall) error {
	start := time.Now()
	defer func() {
		metrics.RegistrationDurationSeconds.WithLabelValues("register").Observe(time.Since(start).Seconds())
	}()

	logger := r.logger.WithContext(ctx).With("requestNamespace", namespace, "pluginId", install.ID, "version", install.Version)

	client, err := r.GetClient()
	if err != nil {
		logger.Error("Failed to get plugin client", "error", err)
		metrics.RegistrationOperationsTotal.WithLabelValues("register", "error").Inc()
		return err
	}
	identifier := resource.Identifier{
		Namespace: namespace,
		Name:      install.ID,
	}

	existing, err := client.Get(ctx, identifier)
	if err != nil && !errorsK8s.IsNotFound(err) {
		logger.Error("Failed to get existing plugin", "error", err)
		metrics.RegistrationOperationsTotal.WithLabelValues("register", "error").Inc()
		return err
	}

	if existing != nil {
		if install.ShouldUpdate(existing) {
			_, err = client.Update(ctx, install.ToPluginInstallV0Alpha1(namespace), resource.UpdateOptions{ResourceVersion: existing.ResourceVersion})
			if err != nil {
				logger.Error("Failed to update plugin", "error", err)
				metrics.RegistrationOperationsTotal.WithLabelValues("register", "error").Inc()
				return err
			}
			metrics.RegistrationOperationsTotal.WithLabelValues("register", "success").Inc()
			return nil
		}
		metrics.RegistrationOperationsTotal.WithLabelValues("register", "success").Inc()
		return nil
	}

	_, err = client.Create(ctx, install.ToPluginInstallV0Alpha1(namespace), resource.CreateOptions{})
	if err != nil {
		logger.Error("Failed to create plugin", "error", err)
		metrics.RegistrationOperationsTotal.WithLabelValues("register", "error").Inc()
		return err
	}
	metrics.RegistrationOperationsTotal.WithLabelValues("register", "success").Inc()
	return nil
}

// Unregister removes a plugin install from the registry.
func (r *InstallRegistrar) Unregister(ctx context.Context, namespace string, name string, source Source) error {
	start := time.Now()
	defer func() {
		metrics.RegistrationDurationSeconds.WithLabelValues("unregister").Observe(time.Since(start).Seconds())
	}()

	logger := r.logger.WithContext(ctx).With("requestNamespace", namespace, "pluginId", name, "source", source)

	client, err := r.GetClient()
	if err != nil {
		logger.Error("Failed to get plugin client", "error", err)
		metrics.RegistrationOperationsTotal.WithLabelValues("unregister", "error").Inc()
		return err
	}
	identifier := resource.Identifier{
		Namespace: namespace,
		Name:      name,
	}
	existing, err := client.Get(ctx, identifier)
	if err != nil && !errorsK8s.IsNotFound(err) {
		logger.Error("Failed to get existing plugin", "error", err)
		metrics.RegistrationOperationsTotal.WithLabelValues("unregister", "error").Inc()
		return err
	}
	// if the plugin doesn't exist, nothing to unregister
	if existing == nil {
		metrics.RegistrationOperationsTotal.WithLabelValues("unregister", "success").Inc()
		return nil
	}
	// if the source is different, do not unregister
	if existingSource, ok := existing.Annotations[PluginInstallSourceAnnotation]; ok && existingSource != source {
		metrics.RegistrationOperationsTotal.WithLabelValues("unregister", "success").Inc()
		return nil
	}
	err = client.Delete(ctx, identifier, resource.DeleteOptions{})
	if err != nil {
		logger.Error("Failed to delete plugin", "error", err)
		metrics.RegistrationOperationsTotal.WithLabelValues("unregister", "error").Inc()
		return err
	}
	metrics.RegistrationOperationsTotal.WithLabelValues("unregister", "success").Inc()
	return err
}
