package appinstaller

import (
	"context"
	"fmt"
	"strings"

	"github.com/emicklei/go-restful/v3"
	"k8s.io/apimachinery/pkg/runtime/schema"
	"k8s.io/apiserver/pkg/registry/generic"
	genericregistry "k8s.io/apiserver/pkg/registry/generic/registry"
	genericrest "k8s.io/apiserver/pkg/registry/rest"
	genericapiserver "k8s.io/apiserver/pkg/server"
	serverstorage "k8s.io/apiserver/pkg/server/storage"

	appsdkapiserver "github.com/grafana/grafana-app-sdk/k8s/apiserver"
	"github.com/grafana/grafana-app-sdk/logging"
	grafanaregistry "github.com/grafana/grafana/pkg/apiserver/registry/generic"
	grafanarest "github.com/grafana/grafana/pkg/apiserver/rest"
	"github.com/grafana/grafana/pkg/services/apiserver/builder"
	"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
	grafanaapiserveroptions "github.com/grafana/grafana/pkg/services/apiserver/options"
	"github.com/grafana/grafana/pkg/storage/legacysql/dualwrite"
)

var _ appsdkapiserver.GenericAPIServer = (*serverWrapper)(nil)

type serverWrapper struct {
	ctx               context.Context
	GenericAPIServer  appsdkapiserver.GenericAPIServer
	installer         appsdkapiserver.AppInstaller
	restOptionsGetter generic.RESTOptionsGetter
	storageOpts       *grafanaapiserveroptions.StorageOptions
	kvStore           grafanarest.NamespacedKVStore
	lock              serverLock
	namespaceMapper   request.NamespaceMapper
	dualWriteService  dualwrite.Service
	dualWriterMetrics *grafanarest.DualWriterMetrics
	builderMetrics    *builder.BuilderMetrics
	apiResourceConfig *serverstorage.ResourceConfig
}

func (s *serverWrapper) InstallAPIGroup(apiGroupInfo *genericapiserver.APIGroupInfo) error {
	log := logging.FromContext(s.ctx)
	for v, storageMap := range apiGroupInfo.VersionedResourcesStorageMap {
		for storagePath, restStorage := range storageMap {
			legacyProvider, dualWriteSupported := s.installer.(LegacyStorageProvider)
			resource, err := getResourceFromStoragePath(storagePath)
			if err != nil {
				return err
			}
			gr := schema.GroupResource{
				Group:    s.installer.ManifestData().Group,
				Resource: resource,
			}
			gvr := gr.WithVersion(v)
			if s.apiResourceConfig != nil && !s.apiResourceConfig.ResourceEnabled(gvr) {
				log.Debug("Skipping storage for disabled resource", "gvr", gvr.String(), "storagePath", storagePath)
				delete(apiGroupInfo.VersionedResourcesStorageMap[v], storagePath)
				continue
			}
			storage := s.configureStorage(gr, dualWriteSupported, restStorage)
			if dualWriteSupported {
				if unifiedStorage, ok := storage.(grafanarest.Storage); ok {
					log.Debug("Configuring dual writer for storage", "resource", gr.String(), "version", v, "storagePath", storagePath)
					storage, err = NewDualWriter(
						s.ctx,
						gr,
						s.storageOpts,
						legacyProvider.GetLegacyStorage(gr.WithVersion(v)),
						unifiedStorage,
						s.kvStore,
						s.lock,
						s.namespaceMapper,
						s.dualWriteService,
						s.dualWriterMetrics,
						s.builderMetrics,
					)
					if err != nil {
						return err
					}
				} else if statusRest, ok := storage.(*appsdkapiserver.StatusREST); ok {
					parentPath := strings.TrimSuffix(storagePath, "/status")
					parentStore, ok := apiGroupInfo.VersionedResourcesStorageMap[v][parentPath]
					if ok {
						if _, isMode4or5 := parentStore.(*genericregistry.Store); !isMode4or5 {
							// When legacy resources have status, the dual writing must be handled explicitly
							if statusProvider, ok := s.installer.(LegacyStatusProvider); ok {
								storage = statusProvider.GetLegacyStatus(gr.WithVersion(v), statusRest)
							} else {
								log.Warn("skipped registering status sub-resource that does not support dual writing",
									"resource", gr.String(), "version", v, "storagePath", storagePath)
								continue
							}
						}
					}
				}
			}
			apiGroupInfo.VersionedResourcesStorageMap[v][storagePath] = storage
		}
	}

	return s.GenericAPIServer.InstallAPIGroup(apiGroupInfo)
}

func getResourceFromStoragePath(storagePath string) (string, error) {
	parts := strings.Split(storagePath, "/")
	if len(parts) < 1 {
		return "", fmt.Errorf("invalid storage path: %s", storagePath)
	}
	return parts[0], nil
}

func (s *serverWrapper) configureStorage(gr schema.GroupResource, dualWriteSupported bool, storage genericrest.Storage) genericrest.Storage {
	if gs, ok := storage.(*genericregistry.Store); ok {
		// if dual write is supported, we need to modify the update strategy
		// this is not needed for the status store
		if dualWriteSupported {
			gs.UpdateStrategy = &updateStrategyWrapper{
				RESTUpdateStrategy: gs.UpdateStrategy,
			}
		}
		gs.KeyFunc = grafanaregistry.NamespaceKeyFunc(gr)
		gs.KeyRootFunc = grafanaregistry.KeyRootFunc(gr)
		return gs
	}

	// if the storage is a status store, we need to extract the underlying generic registry store
	if statusStore, ok := storage.(*appsdkapiserver.StatusREST); ok {
		statusStore.Store.KeyFunc = grafanaregistry.NamespaceKeyFunc(gr)
		statusStore.Store.KeyRootFunc = grafanaregistry.KeyRootFunc(gr)
		return statusStore
	}

	// if the storage is a subresource store, we need to extract the underlying generic registry store
	if subresourceStore, ok := storage.(*appsdkapiserver.SubresourceREST); ok {
		subresourceStore.Store.KeyFunc = grafanaregistry.NamespaceKeyFunc(gr)
		subresourceStore.Store.KeyRootFunc = grafanaregistry.KeyRootFunc(gr)
		return subresourceStore
	}

	return storage
}

func (s *serverWrapper) RegisteredWebServices() []*restful.WebService {
	return s.GenericAPIServer.RegisteredWebServices()
}
