package dispatcher

import (
	"context"
	"fmt"
	"strings"

	"github.com/go-logr/logr"
	"github.com/prometheus/client_golang/prometheus"
	"github.com/tidbcloud/shared-provider/pkg/utils"
	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/client-go/tools/record"
	"sigs.k8s.io/controller-runtime/pkg/cache"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/controller"
	"sigs.k8s.io/controller-runtime/pkg/handler"
	"sigs.k8s.io/controller-runtime/pkg/source"

	cloudv1 "github.com/tidbcloud/infra-api/api/v1"
	"github.com/tidbcloud/shared-provider/pkg/manager"
	"github.com/tidbcloud/shared-provider/pkg/metrics"
	"github.com/tidbcloud/shared-provider/pkg/types"
	"github.com/tidbcloud/shared-provider/pkg/utils/labels"
)

type NetworkDispatcherConfig struct {
	Manager manager.Manager

	KubeClusterConfigMapName      string
	KubeClusterConfigMapNamespace string

	MaxPerKubeCluster int
}

func NewNetworkDispatcher(cfg *NetworkDispatcherConfig) Dispatcher {
	n := &network{
		KubeClusterConfigMapName:      cfg.KubeClusterConfigMapName,
		KubeClusterConfigMapNamespace: cfg.KubeClusterConfigMapNamespace,

		MaxPerKubeCluster: cfg.MaxPerKubeCluster,
	}
	n.InfraClient = cfg.Manager.InfraCluster().GetClient()
	n.InfraCache = cfg.Manager.InfraCluster().GetCache()
	n.InfraRecorder = cfg.Manager.InfraCluster().GetEventRecorderFor("DispatcherNetwork")
	n.Cache = cfg.Manager.GetCache()
	n.Logger = cfg.Manager.GetLogger().WithValues("dispatcher", "network")

	return n
}

type network struct {
	Logger        logr.Logger
	InfraClient   client.Client
	InfraCache    cache.Cache
	InfraRecorder record.EventRecorder

	Cache cache.Cache

	KubeClusterConfigMapName      string
	KubeClusterConfigMapNamespace string

	MaxPerKubeCluster int
}

func (n *network) New() client.Object {
	return &cloudv1.Network{}
}

func (n *network) Watch(c controller.Controller) error {
	ptp := utils.GetPredicateForProviderTypes(types.ProviderTypeGCPSharedTier, types.ProviderTypeAWSSharedTier)

	if err := c.Watch(
		source.NewKindWithCache(&cloudv1.Network{}, n.InfraCache),
		&handler.EnqueueRequestForObject{},
		ptp,
	); err != nil {
		return fmt.Errorf("can't watch resource: %w", err)
	}

	if err := c.Watch(
		source.NewKindWithCache(&corev1.ConfigMap{}, n.Cache),
		handler.Funcs{
			CreateFunc: n.handleCreateConfigMapEvent,
			UpdateFunc: n.handleUpdateConfigMapEvent,
		},
	); err != nil {
		return fmt.Errorf("can't watch configmap: %w", err)
	}

	return nil
}

func (n *network) Schedule(ctx context.Context, obj client.Object) (string, error) {
	net, ok := obj.(*cloudv1.Network)
	if !ok {
		return "", fmt.Errorf("can't assert obj as network")
	}

	if net.Spec.Provider != types.ProviderTypeGCPSharedTier && net.Spec.Provider != types.ProviderTypeAWSSharedTier {
		return "", nil
	}

	kubeClusterName := labels.GetKubeClusterFromLabels(net.Labels)
	// Do nothing if cluster name has been set
	if kubeClusterName != "" {
		return "", nil
	}

	kubeClusterNames, err := getAvailableKubeClusters(
		ctx,
		n.Cache,
		n.KubeClusterConfigMapNamespace,
		n.KubeClusterConfigMapName,
		net.Spec.Region,
	)

	if err != nil {
		return "", err
	}

	if len(kubeClusterNames) == 0 {
		n.InfraRecorder.Eventf(net,
			corev1.EventTypeWarning,
			"RegionIsNotSupported",
			"now region %s is not supported",
			net.Spec.Region,
		)
	}

	return n.schedule(ctx, net, kubeClusterNames)
}

func (n *network) schedule(ctx context.Context, net *cloudv1.Network, cs []string) (string, error) {
	scheduleTo, err := getPreferedKubeCluster(ctx, n.InfraCache, n.MaxPerKubeCluster, cs)
	if err != nil {
		return "", err
	}

	if scheduleTo == "" {
		n.InfraRecorder.Eventf(net,
			corev1.EventTypeWarning,
			"RegionHasNoKubeClusters",
			"now region %s has no available kubernetes clusters",
			net.Spec.Region,
		)
		return "", nil
	}

	return scheduleTo, nil
}

func (n *network) DeleteMetrics(key client.ObjectKey) {
	n.Logger.Info("delete metrics", "namespace", key.Namespace, "name", key.Name)
	metrics.NetworkPhase.Delete(prometheus.Labels{
		"namespace": key.Namespace,
		"network":   key.Name,
	})
	metrics.NetworkCreationTime.Delete(prometheus.Labels{
		"namespace": key.Namespace,
		"network":   key.Name,
	})
	metrics.NetworkRescheduleFailedReason.Delete(prometheus.Labels{
		"namespace": key.Namespace,
		"network":   key.Name,
	})
}

func (n *network) UpdateMetrics(obj client.Object) {
	n.Logger.Info("update metrics", "namespace", obj.GetNamespace(), "name", obj.GetName())

	net, ok := obj.(*cloudv1.Network)
	if !ok {
		panic("can't assert object as network")
	}

	reason, failed := isRescheduleFailed(net)
	if failed {
		metrics.NetworkRescheduleFailedReason.Set(prometheus.Labels{
			"provider_type": net.Spec.Provider,
			"namespace":     net.Namespace,
			"network":       net.Name,
			"reason":        reason,
		}, 1)
	} else {
		metrics.NetworkRescheduleFailedReason.Delete(prometheus.Labels{
			"namespace": net.Namespace,
			"network":   net.Name,
		})
	}

	metrics.NetworkPhase.Set(prometheus.Labels{
		"provider_type": net.Spec.Provider,
		"namespace":     net.Namespace,
		"network":       net.Name,
		"region":        net.Spec.Region,
		"kubernetes":    labels.GetKubeClusterFromLabels(net.Labels),
		"phase":         net.Status.Phase,
	}, 1)

	metrics.NetworkCreationTime.With(prometheus.Labels{
		"provider_type": net.Spec.Provider,
		"namespace":     net.Namespace,
		"network":       net.Name,
	}).Set(float64(net.CreationTimestamp.Unix()))
}

func getAvailableKubeClusters(ctx context.Context, c client.Reader, ns, cmName, region string) ([]string, error) {
	cm := corev1.ConfigMap{}
	cmKey := client.ObjectKey{
		Name:      cmName,
		Namespace: ns,
	}
	if err := c.Get(ctx, cmKey, &cm); err != nil {
		return nil, fmt.Errorf("can't get configmap %v: %w", cmKey, err)
	}

	strClusters, ok := cm.Data[region]
	if !ok {
		return nil, nil
	}

	return strings.Split(strClusters, ","), nil
}

func getPreferedKubeCluster(ctx context.Context, c client.Reader, max int, kubeClusterNames []string) (string, error) {
	min := max
	scheduleTo := ""
	for _, kubeClusterName := range kubeClusterNames {
		kubeClusterName = strings.TrimSpace(kubeClusterName)
		if kubeClusterName == "" {
			continue
		}

		selector, err := metav1.LabelSelectorAsSelector(&metav1.LabelSelector{
			MatchLabels: map[string]string{
				labels.LabelKeyKubernetesCluster: kubeClusterName,
			},
			MatchExpressions: []metav1.LabelSelectorRequirement{{
				Key:      cloudv1.ProviderTypeLabel,
				Operator: metav1.LabelSelectorOpIn,
				Values:   []string{types.ProviderTypeGCPSharedTier, types.ProviderTypeAWSSharedTier},
			}},
		})
		if err != nil {
			return "", fmt.Errorf("can't get LabelSelectorAsSelector in %s: %w", kubeClusterName, err)
		}

		cl := cloudv1.ClusterList{}
		if err := c.List(ctx, &cl, client.MatchingLabelsSelector{Selector: selector}); err != nil {
			return "", fmt.Errorf("can't list tidb clusters in %s: %w", kubeClusterName, err)
		}

		if min > len(cl.Items) {
			min = len(cl.Items)
			scheduleTo = kubeClusterName
		}
	}

	return scheduleTo, nil
}
