package agentk

import (
	"context"
	"fmt"
	"log/slog"
	"net/http"
	"time"

	notificationv1 "github.com/fluxcd/notification-controller/api/v1"
	sourcev1 "github.com/fluxcd/source-controller/api/v1"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/module/flux"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/module/flux/rpc"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/module/modagentk"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/tool/logz"
	"gitlab.com/gitlab-org/cluster-integration/tunnel/tool/retry"
	"k8s.io/apimachinery/pkg/api/meta"
	"k8s.io/apimachinery/pkg/runtime/schema"
	"k8s.io/client-go/dynamic"
	"k8s.io/client-go/dynamic/dynamicinformer"
	"k8s.io/client-go/informers"
	"k8s.io/client-go/kubernetes"
	"k8s.io/client-go/rest"
	"k8s.io/client-go/tools/cache"
	"k8s.io/client-go/transport"
)

const (
	// resyncDuration defines the duration for the shared informer cache resync interval.
	resyncDuration = 10 * time.Minute

	reconcileProjectsInitBackoff   = 10 * time.Second
	reconcileProjectsMaxBackoff    = 5 * time.Minute
	reconcileProjectsResetDuration = 10 * time.Minute
	reconcileProjectsBackoffFactor = 2.0
	reconcileProjectsJitter        = 5.0
)

var (
	requiredFluxCRDs = [...]schema.GroupVersionResource{
		sourcev1.GroupVersion.WithResource("gitrepositories"),
		notificationv1.GroupVersion.WithResource("receivers"),
	}
)

type Factory struct{}

func (f *Factory) IsProducingLeaderModules() bool {
	return true
}

func (f *Factory) New(config *modagentk.Config) (modagentk.Module, error) {
	restConfig, err := config.K8sUtilFactory.ToRESTConfig()
	if err != nil {
		return nil, err
	}

	restConfig = rest.CopyConfig(restConfig)
	restConfig.UserAgent = config.AgentNameVersion

	dynamicClient, err := dynamic.NewForConfig(restConfig)
	if err != nil {
		return nil, err
	}

	mapper, err := config.K8sUtilFactory.ToRESTMapper()
	if err != nil {
		return nil, err
	}
	if !isFluxInstalled(config.Log, mapper) {
		config.Log.Info("Flux could not be detected or the Agent is missing RBAC, skipping module. A restart is required for this to be checked again")
		return nil, nil
	}

	clientset, err := kubernetes.NewForConfig(restConfig)
	if err != nil {
		return nil, err
	}

	receiverClient := dynamicClient.Resource(notificationv1.GroupVersion.WithResource("receivers"))

	kubeAPIURL, _, err := rest.DefaultServerUrlFor(restConfig)
	if err != nil {
		return nil, err
	}
	transportCfg, err := restConfig.TransportConfig()
	if err != nil {
		return nil, err
	}
	kubeAPIRoundTripper, err := transport.New(transportCfg)
	if err != nil {
		return nil, err
	}

	config.RegisterKASAPI(&rpc.GitLabFlux_ServiceDesc)
	return &module{
		log: config.Log,
		runner: &fluxReconciliationRunner{
			informersFactory: func() (informers.GenericInformer, informers.GenericInformer, cache.Indexer) {
				informerFactory := dynamicinformer.NewDynamicSharedInformerFactory(dynamicClient, resyncDuration)
				gitRepositoryInformer := informerFactory.ForResource(sourcev1.GroupVersion.WithResource("gitrepositories"))
				receiverInformer := informerFactory.ForResource(notificationv1.GroupVersion.WithResource("receivers"))
				receiverIndexer := receiverInformer.Informer().GetIndexer()
				return gitRepositoryInformer, receiverInformer, receiverIndexer
			},
			clientFactory: func(ctx context.Context, cfgURL string, receiverIndexer cache.Indexer) (*client, error) {
				agentKey, err := config.API.GetAgentKey(ctx)
				if err != nil {
					return nil, err
				}

				rt, err := newGitRepositoryReconcileTrigger(cfgURL, kubeAPIURL, kubeAPIRoundTripper, http.DefaultTransport)
				if err != nil {
					return nil, err
				}

				return newClient(
					config.Log,
					config.API,
					agentKey,
					rpc.NewGitLabFluxClient(config.KASConn),
					retry.PollConfig{
						Interval: 0,
						Backoff: retry.NewExponentialBackoff(
							reconcileProjectsInitBackoff,
							reconcileProjectsMaxBackoff,
							reconcileProjectsResetDuration,
							reconcileProjectsBackoffFactor,
							reconcileProjectsJitter,
						),
					},
					receiverIndexer,
					rt,
					clientset.CoreV1(),
				)
			},
			controllerFactory: func(ctx context.Context, gitRepositoryInformer informers.GenericInformer, receiverInformer informers.GenericInformer, projectReconciler projectReconciler) (controller, error) {
				agentKey, err := config.API.GetAgentKey(ctx)
				if err != nil {
					return nil, err
				}
				gitLabExternalURL, err := config.API.GetGitLabExternalURL(ctx)
				if err != nil {
					return nil, err
				}

				return newGitRepositoryController(ctx, config.Log, config.API, agentKey, gitLabExternalURL, gitRepositoryInformer, receiverInformer, projectReconciler, receiverClient, clientset.CoreV1())
			},
		},
	}, nil
}

func (f *Factory) Name() string {
	return flux.ModuleName
}

func isFluxInstalled(log *slog.Logger, mapper meta.RESTMapper) bool {
	for _, crd := range requiredFluxCRDs {
		ok, err := checkResourceExists(mapper, crd)
		if err != nil {
			log.Error("Unable to check if CRD is installed", logz.GVR(crd), logz.Error(err))
			return false
		}
		if !ok {
			log.Debug("Required Flux CRD is not established", logz.GVR(crd))
			return false
		}
	}
	return true
}

func checkResourceExists(mapper meta.RESTMapper, gvr schema.GroupVersionResource) (bool, error) {
	_, err := mapper.ResourcesFor(gvr)
	if err != nil {
		// the mapper will return this error if there are
		// zero resources found, so this is not actually
		// something we should treat as an error
		if _, ok := err.(*meta.NoResourceMatchError); ok { //nolint:errorlint
			return false, nil
		}
		return false, fmt.Errorf("finding resources: %w", err)
	}
	return true, nil
}
