package dispatcher

import (
	"context"
	"fmt"
	"strings"

	"github.com/go-logr/logr"
	"github.com/prometheus/client_golang/prometheus"
	"github.com/tidbcloud/shared-provider/pkg/utils"
	apierrors "k8s.io/apimachinery/pkg/api/errors"
	"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"
)

const (
	TypeBackup  = "Backup"
	TypeRestore = "Restore"
	TypeImport  = "Import"
)

type (
	// TODO: refactor all funcs
	GetNetworkFunc      func(ctx context.Context, r client.Reader, obj client.Object) (*cloudv1.Network, error)
	NewFunc             func() client.Object
	GetMetricLabelsFunc func(client.Object, bool) map[string]string
)

type OthersDispatcherConfig struct {
	Manager manager.Manager
	Type    string
}

func NewOthersDispatcher(cfg *OthersDispatcherConfig) (Dispatcher, error) {
	d := &others{}
	err := d.attachNewGetFn(cfg.Type)
	if err != nil {
		return nil, err
	}

	d.InfraClient = cfg.Manager.InfraCluster().GetClient()
	d.InfraCache = cfg.Manager.InfraCluster().GetCache()
	d.InfraRecorder = cfg.Manager.InfraCluster().GetEventRecorderFor("Dispatcher" + cfg.Type)
	d.Logger = cfg.Manager.GetLogger().WithValues("dispatcher", strings.ToLower(cfg.Type))

	return d, nil
}

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

	newFn NewFunc
	getFn GetNetworkFunc

	getMetricLabelsFn GetMetricLabelsFunc

	phaseMetrics        *metrics.GaugeVec
	creationTimeMetrics *prometheus.GaugeVec
}

func (d *others) attachNewGetFn(t string) error {
	switch t {
	case TypeBackup:
		d.newFn = func() client.Object { return &cloudv1.Backup{} }
		d.getFn = GetNetworkFromBackup
		d.getMetricLabelsFn = getBackupMetricLabels
		d.phaseMetrics = metrics.BackupPhase
		d.creationTimeMetrics = metrics.BackupCreationTime
	case TypeRestore:
		d.newFn = func() client.Object { return &cloudv1.Restore{} }
		d.getFn = GetNetworkFromRestore
		d.getMetricLabelsFn = getRestoreMetricLabels
		d.phaseMetrics = metrics.RestorePhase
		d.creationTimeMetrics = metrics.RestoreCreationTime
	case TypeImport:
		d.newFn = func() client.Object { return &cloudv1.Import{} }
		d.getFn = GetNetworkFromImport
		d.getMetricLabelsFn = getImportMetricLabels
		d.phaseMetrics = metrics.ImportPhase
		d.creationTimeMetrics = metrics.ImportCreationTime
	default:
		return fmt.Errorf("unknown type %s for dispatcher", t)
	}

	return nil
}

func (d *others) New() client.Object {
	return d.newFn()
}

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

	obj := d.newFn()

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

	return nil
}

func (d *others) Schedule(ctx context.Context, obj client.Object) (string, error) {
	// If object has been scheduled, just return
	// NOTE: It maybe more reasonable to reschedule objects
	scheduled := labels.GetKubeClusterFromLabels(obj.GetLabels())
	if scheduled != "" {
		return "", nil
	}

	net, err := d.getFn(ctx, d.InfraCache, obj)
	if err != nil {
		return "", err
	}
	if net == nil {
		return "", nil
	}
	if net.Spec.Provider != types.ProviderTypeGCPSharedTier && net.Spec.Provider != types.ProviderTypeAWSSharedTier {
		return "", nil
	}

	kubeClusterName := labels.GetKubeClusterFromLabels(net.Labels)
	if kubeClusterName == "" {
		return "", fmt.Errorf("network %s/%s is unscheduled, waiting", net.Namespace, net.Name)
	}

	return kubeClusterName, nil
}

func (d *others) DeleteMetrics(key client.ObjectKey) {
	d.Logger.Info("delete metrics", "namespace", key.Namespace, "name", key.Name)

	// TODO: refactor to avoid new an object
	obj := d.New()
	obj.SetNamespace(key.Namespace)
	obj.SetName(key.Name)
	labels := d.getMetricLabelsFn(obj, true)

	d.phaseMetrics.Delete(prometheus.Labels(labels))
	d.creationTimeMetrics.Delete(prometheus.Labels(labels))
}

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

	labels := d.getMetricLabelsFn(obj, false)
	d.phaseMetrics.Set(prometheus.Labels(labels), 1)

	keyLabels := d.getMetricLabelsFn(obj, true)
	d.creationTimeMetrics.With(prometheus.Labels(keyLabels)).Set(float64(obj.GetCreationTimestamp().Unix()))
}

func getNetworkFromClusterKey(ctx context.Context, r client.Reader, key client.ObjectKey) (*cloudv1.Network, error) {
	c := cloudv1.Cluster{}
	if err := r.Get(ctx, key, &c); err != nil {
		// NOTE: now cluster may be not found, just ignore this error
		// TODO: make resources related with network
		if apierrors.IsNotFound(err) {
			return nil, nil
		}
		return nil, err
	}

	return getNetworkFromCluster(ctx, r, &c)
}

func GetNetworkFromBackup(ctx context.Context, r client.Reader, obj client.Object) (*cloudv1.Network, error) {
	backup, ok := obj.(*cloudv1.Backup)
	if !ok {
		return nil, fmt.Errorf("can't assert obj as backup")
	}

	clusterKey := client.ObjectKey{
		Namespace: obj.GetNamespace(),
		Name:      backup.Spec.ClusterName,
	}

	return getNetworkFromClusterKey(ctx, r, clusterKey)
}

func GetNetworkFromRestore(ctx context.Context, r client.Reader, obj client.Object) (*cloudv1.Network, error) {
	restore, ok := obj.(*cloudv1.Restore)
	if !ok {
		return nil, fmt.Errorf("can't assert obj as restore")
	}

	clusterKey := client.ObjectKey{
		Namespace: obj.GetNamespace(),
		Name:      restore.Spec.ClusterName,
	}
	return getNetworkFromClusterKey(ctx, r, clusterKey)
}

func GetNetworkFromImport(ctx context.Context, r client.Reader, obj client.Object) (*cloudv1.Network, error) {
	im, ok := obj.(*cloudv1.Import)
	if !ok {
		return nil, fmt.Errorf("can't assert obj as import")
	}

	clusterKey := client.ObjectKey{
		Namespace: obj.GetNamespace(),
		Name:      im.Spec.ClusterName,
	}
	return getNetworkFromClusterKey(ctx, r, clusterKey)
}

func getBackupMetricLabels(obj client.Object, onlyKey bool) map[string]string {
	backup, ok := obj.(*cloudv1.Backup)
	if !ok {
		panic("can't assert obj as backup")
	}
	m := map[string]string{
		"provider_type": labels.GetProviderTypeFromLabels(backup.Labels),
		"namespace":     backup.Namespace,
		"backup":        backup.Name,
	}
	if !onlyKey {
		m["kubernetes"] = labels.GetKubeClusterFromLabels(backup.Labels)
		m["tidb"] = backup.Spec.ClusterName
		m["phase"] = string(backup.Status.Phase)
	}
	return m
}

func getRestoreMetricLabels(obj client.Object, onlyKey bool) map[string]string {
	restore, ok := obj.(*cloudv1.Restore)
	if !ok {
		panic("can't assert obj as restore")
	}
	m := map[string]string{
		"provider_type": labels.GetProviderTypeFromLabels(restore.Labels),
		"namespace":     restore.Namespace,
		"restore":       restore.Name,
	}
	if !onlyKey {
		m["kubernetes"] = labels.GetKubeClusterFromLabels(restore.Labels)
		m["tidb"] = restore.Spec.ClusterName
		m["phase"] = string(restore.Status.Phase)
	}
	return m
}

func getImportMetricLabels(obj client.Object, onlyKey bool) map[string]string {
	im, ok := obj.(*cloudv1.Import)
	if !ok {
		panic("can't assert obj as import")
	}
	m := map[string]string{
		"provider_type": labels.GetProviderTypeFromLabels(im.Labels),
		"namespace":     im.Namespace,
		"import":        im.Name,
	}
	if !onlyKey {
		m["kubernetes"] = labels.GetKubeClusterFromLabels(im.Labels)
		m["tidb"] = im.Spec.ClusterName
		m["phase"] = string(im.Status.Phase)
	}
	return m
}
