package sso

import (
	"context"
	"errors"
	"fmt"

	commonv1 "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1"
	"github.com/grafana/grafana/pkg/apimachinery/identity"
	"go.opentelemetry.io/otel/trace"
	apierrors "k8s.io/apimachinery/pkg/api/errors"
	"k8s.io/apimachinery/pkg/apis/meta/internalversion"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/types"
	"k8s.io/apiserver/pkg/registry/rest"

	iamv0 "github.com/grafana/grafana/pkg/apis/iam/v0alpha1"
	"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
	"github.com/grafana/grafana/pkg/services/ssosettings"
	ssomodels "github.com/grafana/grafana/pkg/services/ssosettings/models"
)

var (
	_ rest.Storage              = (*LegacyStore)(nil)
	_ rest.Scoper               = (*LegacyStore)(nil)
	_ rest.Getter               = (*LegacyStore)(nil)
	_ rest.Lister               = (*LegacyStore)(nil)
	_ rest.Updater              = (*LegacyStore)(nil)
	_ rest.SingularNameProvider = (*LegacyStore)(nil)
	_ rest.GracefulDeleter      = (*LegacyStore)(nil)
)

var resource = iamv0.SSOSettingResourceInfo

func NewLegacyStore(service ssosettings.Service, tracer trace.Tracer) *LegacyStore {
	return &LegacyStore{service, tracer}
}

type LegacyStore struct {
	service ssosettings.Service
	tracer  trace.Tracer
}

// Destroy implements rest.Storage.
func (s *LegacyStore) Destroy() {}

// NamespaceScoped implements rest.Scoper.
func (s *LegacyStore) NamespaceScoped() bool {
	// this is maybe incorrect
	return true
}

// GetSingularName implements rest.SingularNameProvider.
func (s *LegacyStore) GetSingularName() string {
	return resource.GetSingularName()
}

// New implements rest.Storage.
func (s *LegacyStore) New() runtime.Object {
	return resource.NewFunc()
}

// ConvertToTable implements rest.Lister.
func (s *LegacyStore) ConvertToTable(ctx context.Context, object runtime.Object, tableOptions runtime.Object) (*metav1.Table, error) {
	return resource.TableConverter().ConvertToTable(ctx, object, tableOptions)
}

// NewList implements rest.Lister.
func (s *LegacyStore) NewList() runtime.Object {
	return resource.NewListFunc()
}

// List implements rest.Lister.
func (s *LegacyStore) List(ctx context.Context, options *internalversion.ListOptions) (runtime.Object, error) {
	ctx, span := s.tracer.Start(ctx, "sso.List")
	defer span.End()

	ns, _ := request.NamespaceInfoFrom(ctx, false)

	settings, err := s.service.List(ctx)
	if err != nil {
		return nil, fmt.Errorf("failed to list sso settings: %w", err)
	}

	list := &iamv0.SSOSettingList{}
	for _, s := range settings {
		list.Items = append(list.Items, mapToObject(ns.Value, s))
	}

	return list, nil
}

// Get implements rest.Getter.
func (s *LegacyStore) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) {
	ctx, span := s.tracer.Start(ctx, "sso.Get")
	defer span.End()

	ns, _ := request.NamespaceInfoFrom(ctx, false)

	setting, err := s.service.GetForProviderWithRedactedSecrets(ctx, name)
	if err != nil {
		if errors.Is(err, ssosettings.ErrNotFound) {
			return nil, resource.NewNotFound(name)
		}
		return nil, err
	}

	object := mapToObject(ns.Value, setting)
	return &object, nil
}

// Update implements rest.Updater.
func (s *LegacyStore) Update(
	ctx context.Context,
	name string,
	objInfo rest.UpdatedObjectInfo,
	_ rest.ValidateObjectFunc,
	_ rest.ValidateObjectUpdateFunc,
	_ bool,
	_ *metav1.UpdateOptions,
) (runtime.Object, bool, error) {
	ctx, span := s.tracer.Start(ctx, "sso.Update")
	defer span.End()

	const created = false
	ident, err := identity.GetRequester(ctx)
	if err != nil {
		return nil, created, err
	}

	old, err := s.Get(ctx, name, nil)
	if err != nil {
		return old, created, err
	}

	obj, err := objInfo.UpdatedObject(ctx, old)
	if err != nil {
		return old, created, err
	}

	setting, ok := obj.(*iamv0.SSOSetting)
	if !ok {
		return old, created, errors.New("expected ssosetting after update")
	}

	if err := s.service.Upsert(ctx, mapToModel(setting), ident); err != nil {
		return old, created, err
	}

	updated, err := s.Get(ctx, name, nil)
	return updated, created, err
}

// Delete implements rest.GracefulDeleter.
func (s *LegacyStore) Delete(
	ctx context.Context,
	name string,
	_ rest.ValidateObjectFunc,
	options *metav1.DeleteOptions,
) (runtime.Object, bool, error) {
	ctx, span := s.tracer.Start(ctx, "sso.Delete")
	defer span.End()

	obj, err := s.Get(ctx, name, nil)
	if err != nil {
		return obj, false, err
	}

	old, ok := obj.(*iamv0.SSOSetting)
	if !ok {
		return obj, false, errors.New("expected ssosetting")
	}

	// FIXME(kalleep): this should probably be validated in transaction
	if options.Preconditions != nil && options.Preconditions.ResourceVersion != nil {
		if *options.Preconditions.ResourceVersion != old.GetResourceVersion() {
			return old, false, apierrors.NewConflict(
				resource.GroupResource(),
				name,
				fmt.Errorf(
					"the ResourceVersion in the precondition (%s) does not match the ResourceVersion in record (%s). The object might have been modified",
					*options.Preconditions.ResourceVersion,
					old.GetResourceVersion(),
				),
			)
		}
	}

	if err := s.service.Delete(ctx, name); err != nil {
		return old, false, err
	}

	// If settings for a provider is deleted from db they will fallback to settings from config file, env or arguments.
	afterDelete, err := s.Get(ctx, name, nil)
	return afterDelete, false, err
}

func mapToObject(ns string, s *ssomodels.SSOSettings) iamv0.SSOSetting {
	source := iamv0.SourceDB
	if s.Source == ssomodels.System {
		source = iamv0.SourceSystem
	}

	version := "0"
	if !s.Updated.IsZero() {
		version = fmt.Sprintf("%d", s.Updated.UnixMilli())
	}

	object := iamv0.SSOSetting{
		ObjectMeta: metav1.ObjectMeta{
			Name:              s.Provider,
			Namespace:         ns,
			UID:               types.UID(s.Provider),
			ResourceVersion:   version,
			CreationTimestamp: metav1.NewTime(s.Updated),
		},
		Spec: iamv0.SSOSettingSpec{
			Source:   source,
			Settings: commonv1.Unstructured{Object: s.Settings},
		},
	}

	return object
}

func mapToModel(obj *iamv0.SSOSetting) *ssomodels.SSOSettings {
	return &ssomodels.SSOSettings{
		Provider: obj.Name,
		Settings: obj.Spec.Settings.Object,
	}
}
