package collectors

import (
	"sort"
	"strings"

	"k8s.io/kube-state-metrics/pkg/collector"
	"k8s.io/kube-state-metrics/pkg/metric"
	"k8s.io/kube-state-metrics/pkg/metrics_store"
	"k8s.io/kube-state-metrics/pkg/options"

	"k8s.io/client-go/tools/cache"

	appsv1 "github.com/openshift/api/apps/v1"
	buildv1 "github.com/openshift/api/build/v1"
	quotav1 "github.com/openshift/api/quota/v1"
	routev1 "github.com/openshift/api/route/v1"
	userv1 "github.com/openshift/api/user/v1"
	"golang.org/x/net/context"
	"k8s.io/klog/v2"
)

type whiteBlackLister interface {
	IsIncluded(string) bool
	IsExcluded(string) bool
}

// Builder helps to build collectors. It follows the builder pattern
// (https://en.wikipedia.org/wiki/Builder_pattern).
type Builder struct {
	apiserver         string
	kubeconfig        string
	namespaces        options.NamespaceList
	ctx               context.Context
	enabledCollectors []string
	whiteBlackList    whiteBlackLister
}

// NewBuilder returns a new builder.
func NewBuilder(
	ctx context.Context,
) *Builder {
	return &Builder{
		ctx: ctx,
	}
}

func (b *Builder) WithApiserver(apiserver string) *Builder {
	b.apiserver = apiserver
	return b
}

func (b *Builder) WithKubeConfig(kubeconfig string) *Builder {
	b.kubeconfig = kubeconfig
	return b
}

// WithEnabledCollectors sets the enabledCollectors property of a Builder.
func (b *Builder) WithEnabledCollectors(c []string) *Builder {
	copy := []string{}
	for _, s := range c {
		copy = append(copy, s)
	}

	sort.Strings(copy)

	b.enabledCollectors = copy
	return b
}

// WithNamespaces sets the namespaces property of a Builder.
func (b *Builder) WithNamespaces(n options.NamespaceList) *Builder {
	b.namespaces = n
	return b
}

// WithWhiteBlackList configures the white or blacklisted metrics to be exposed
// by the collectors build by the Builder
func (b *Builder) WithWhiteBlackList(l whiteBlackLister) *Builder {
	b.whiteBlackList = l
	return b
}

// Build initializes and registers all enabled collectors.
func (b *Builder) Build() []*collector.Collector {
	if b.whiteBlackList == nil {
		panic("whiteBlackList should not be nil")
	}

	collectors := []*collector.Collector{}
	activeCollectorNames := []string{}

	for _, c := range b.enabledCollectors {
		constructor, ok := availableCollectors[c]
		if !ok {
			klog.Fatalf("collector %s is not correct", c)
		}

		collector := constructor(b)
		activeCollectorNames = append(activeCollectorNames, c)
		collectors = append(collectors, collector)

	}

	klog.Infof("Active collectors: %s", strings.Join(activeCollectorNames, ","))

	return collectors
}

var availableCollectors = map[string]func(f *Builder) *collector.Collector{
	"deploymentConfigs":     func(b *Builder) *collector.Collector { return b.buildDeploymentCollector() },
	"buildconfigs":          func(b *Builder) *collector.Collector { return b.buildBuildConfigCollector() },
	"builds":                func(b *Builder) *collector.Collector { return b.buildBuildCollector() },
	"clusterresourcequotas": func(b *Builder) *collector.Collector { return b.buildQuotaCollector() },
	"routes":                func(b *Builder) *collector.Collector { return b.buildRouteCollector() },
	"groups":                func(b *Builder) *collector.Collector { return b.buildGroupCollector() },
}

func (b *Builder) buildRouteCollector() *collector.Collector {
	filteredMetricFamilies := metric.FilterMetricFamilies(b.whiteBlackList, routeMetricFamilies)
	composedMetricGenFuncs := metric.ComposeMetricGenFuncs(filteredMetricFamilies)

	familyHeaders := metric.ExtractMetricFamilyHeaders(filteredMetricFamilies)

	store := metricsstore.NewMetricsStore(
		familyHeaders,
		composedMetricGenFuncs,
	)
	reflectorPerNamespace(b.ctx, &routev1.Route{}, store,
		b.apiserver, b.kubeconfig, b.namespaces, createRouteListWatch)

	return collector.NewCollector(store)
}

func (b *Builder) buildDeploymentCollector() *collector.Collector {
	filteredMetricFamilies := metric.FilterMetricFamilies(b.whiteBlackList, deploymentMetricFamilies)
	composedMetricGenFuncs := metric.ComposeMetricGenFuncs(filteredMetricFamilies)

	familyHeaders := metric.ExtractMetricFamilyHeaders(filteredMetricFamilies)

	store := metricsstore.NewMetricsStore(
		familyHeaders,
		composedMetricGenFuncs,
	)
	reflectorPerNamespace(b.ctx, &appsv1.DeploymentConfig{}, store,
		b.apiserver, b.kubeconfig, b.namespaces, createDeploymentListWatch)

	return collector.NewCollector(store)
}

func (b *Builder) buildBuildConfigCollector() *collector.Collector {
	filteredMetricFamilies := metric.FilterMetricFamilies(b.whiteBlackList, buildconfigMetricFamilies)
	composedMetricGenFuncs := metric.ComposeMetricGenFuncs(filteredMetricFamilies)

	familyHeaders := metric.ExtractMetricFamilyHeaders(filteredMetricFamilies)

	store := metricsstore.NewMetricsStore(
		familyHeaders,
		composedMetricGenFuncs,
	)
	reflectorPerNamespace(b.ctx, &buildv1.BuildConfig{}, store,
		b.apiserver, b.kubeconfig, b.namespaces, createBuildConfigListWatch)

	return collector.NewCollector(store)
}

func (b *Builder) buildBuildCollector() *collector.Collector {
	filteredMetricFamilies := metric.FilterMetricFamilies(b.whiteBlackList, buildMetricFamilies)
	composedMetricGenFuncs := metric.ComposeMetricGenFuncs(filteredMetricFamilies)

	familyHeaders := metric.ExtractMetricFamilyHeaders(filteredMetricFamilies)

	store := metricsstore.NewMetricsStore(
		familyHeaders,
		composedMetricGenFuncs,
	)
	reflectorPerNamespace(b.ctx, &buildv1.Build{}, store,
		b.apiserver, b.kubeconfig, b.namespaces, createBuildListWatch)

	return collector.NewCollector(store)
}

func (b *Builder) buildQuotaCollector() *collector.Collector {
	filteredMetricFamilies := metric.FilterMetricFamilies(b.whiteBlackList, quotaMetricFamilies)
	composedMetricGenFuncs := metric.ComposeMetricGenFuncs(filteredMetricFamilies)

	familyHeaders := metric.ExtractMetricFamilyHeaders(filteredMetricFamilies)

	store := metricsstore.NewMetricsStore(
		familyHeaders,
		composedMetricGenFuncs,
	)
	reflectorPerNamespace(b.ctx, &quotav1.ClusterResourceQuota{}, store,
		b.apiserver, b.kubeconfig, b.namespaces, createClusterResourceQuotaListWatch)

	return collector.NewCollector(store)
}

func (b *Builder) buildGroupCollector() *collector.Collector {
	filteredMetricFamilies := metric.FilterMetricFamilies(b.whiteBlackList, groupMetricFamilies)
	composedMetricGenFuncs := metric.ComposeMetricGenFuncs(filteredMetricFamilies)

	familyHeaders := metric.ExtractMetricFamilyHeaders(filteredMetricFamilies)

	store := metricsstore.NewMetricsStore(
		familyHeaders,
		composedMetricGenFuncs,
	)
	reflectorPerNamespace(b.ctx, &userv1.Group{}, store,
		b.apiserver, b.kubeconfig, b.namespaces, createGroupListWatch)

	return collector.NewCollector(store)
}

// reflectorPerNamespace creates a Kubernetes client-go reflector with the given
// listWatchFunc for each given namespace and registers it with the given store.
func reflectorPerNamespace(
	ctx context.Context,
	expectedType interface{},
	store cache.Store,
	apiserver string,
	kubeconfig string,
	namespaces []string,
	listWatchFunc func(apiserver string, kubeconfig string, ns string) cache.ListWatch,
) {
	for _, ns := range namespaces {
		lw := listWatchFunc(apiserver, kubeconfig, ns)
		reflector := cache.NewReflector(&lw, expectedType, store, 0)
		go reflector.Run(ctx.Done())
	}
}
