package inspect

import (
	"context"
	"fmt"
	"sync"

	"github.com/tidbcloud/infra-provider/infra-inspector/api"
	"github.com/tidbcloud/infra-provider/infra-inspector/pkg/config"
	"github.com/tidbcloud/infra-provider/infra-inspector/pkg/deps"
	"github.com/tidbcloud/infra-provider/infra-inspector/pkg/inspect/engine"
	"github.com/tidbcloud/infra-provider/infra-inspector/pkg/inspect/lister"

	"github.com/go-logr/logr"
	cloudv1 "github.com/tidbcloud/infra-api/api/v1"
	k8sutil "github.com/tidbcloud/infra-provider/pkg/k8s"
	"github.com/tidbcloud/infra-provider/pkg/scheme"
	"golang.org/x/exp/slices"
	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/types"
	"k8s.io/client-go/rest"
	"k8s.io/client-go/tools/clientcmd"
	"k8s.io/client-go/util/workqueue"
	"sigs.k8s.io/controller-runtime/pkg/client"
)

var (
	dedicatedLabelSelector = &metav1.LabelSelector{
		MatchLabels: map[string]string{
			"tags.tidbcloud.com/servicetype": "dedicated",
		},
	}
	concurrency = 10
)

// DataplaneInspector lists all dataplane clusters and inspects resources in each cluster.
type DataplaneInspector struct {
	env            string // dev / staging / prod, it is used to get the kubeconfig of a shoot
	infraClient    client.Client
	gardenerClient client.Client
	engine         engine.Engine
}

func NewDataplaneInspector(env string, deps *deps.Denpendency, ecfg config.EngineConfig) (*DataplaneInspector, error) {
	engine, err := engine.NewEngine(ecfg)
	if err != nil {
		return nil, err
	}

	return &DataplaneInspector{
		env:            env,
		infraClient:    deps.InfraCtrlClient,
		gardenerClient: deps.GardenerCtrlClient,
		engine:         engine,
	}, nil
}

func (i *DataplaneInspector) Inspect(ctx context.Context, listConfig config.ListConfig) ([]*api.Result, error) {
	var err error
	networks := cloudv1.NetworkList{}

	listOpts := &client.ListOptions{}
	if listConfig.Dataplane != nil {
		listOpts.Namespace = listConfig.Dataplane.Namespace
		listOpts.LabelSelector, err = metav1.LabelSelectorAsSelector(listConfig.Dataplane.LabelSelector)
	} else {
		listOpts.LabelSelector, err = metav1.LabelSelectorAsSelector(dedicatedLabelSelector)
	}
	if err != nil {
		return nil, fmt.Errorf("create list option failed: %v", err)
	}

	err = i.infraClient.List(ctx, &networks, listOpts)
	if err != nil {
		return nil, fmt.Errorf("list networks failed: %v", err)
	}

	results, err := i.inspectDataplanesConcurrently(ctx, listConfig, networks.Items)
	if err != nil {
		return nil, err
	}

	return results, nil
}

func (i *DataplaneInspector) inspectDataplanesConcurrently(ctx context.Context, listConfig config.ListConfig, networks []cloudv1.Network) ([]*api.Result, error) {
	logger := logr.FromContextOrDiscard(ctx)
	mu := &sync.Mutex{}
	results := make([]*api.Result, 0, len(networks))

	workqueue.ParallelizeUntil(ctx, concurrency, len(networks), func(index int) {
		network := networks[index]
		key := types.NamespacedName{Namespace: network.Namespace, Name: network.Name}.String()
		target := fmt.Sprintf("dataplane/%s", key)
		logger := logger.WithValues("target", target)

		logger.V(4).Info("begin inspecting a dataplane")

		if ok, reason := predNetwork(network, listConfig.Dataplane); !ok {
			logger.V(4).Info("skip inspect a dataplane", "reason", reason)
			return
		}

		lister, err := i.createListerFromNetwork(ctx, network)
		if err != nil {
			logger.Error(err, "create lister failed")
			return
		}

		inspector := &GenericInspector{
			Target: target,
			Lister: lister,
			Engine: i.engine,
		}
		curResults, err := inspector.Inspect(ctx, listConfig)
		if err != nil {
			logger.Error(err, "inspect dataplane failed")
			return
		}

		logger.V(4).Info("end inspecting a dataplane")
		mu.Lock()
		results = append(results, curResults...)
		mu.Unlock()
	})
	return results, nil
}

func (i *DataplaneInspector) createListerFromNetwork(ctx context.Context, network cloudv1.Network) (lister.ResourceLister, error) {
	var (
		cfg *rest.Config
		err error
	)

	switch network.Spec.Provider {
	case cloudv1.ProviderTypeEKS, cloudv1.ProviderTypeGKE:
		cfg, err = k8sutil.ConfigFromNetwork(ctx, i.infraClient, &network, scheme.Scheme)
	case cloudv1.ProviderAWS, cloudv1.ProviderGCP:
		cfg, err = shootKubeConfigFromNetwork(ctx, i.gardenerClient, i.env, &network)
	}

	if err != nil {
		return nil, fmt.Errorf("get dataplane config failed: %v", err)
	}

	l, err := lister.NewResourceListerFromConfig(cfg)
	if err != nil {
		return nil, fmt.Errorf("create resource lister failed: %v", err)
	}

	return l, nil
}

func shootKubeConfigFromNetwork(ctx context.Context, client client.Client, env string, network *cloudv1.Network) (*rest.Config, error) {
	status, err := cloudv1.GardenerNetworkStatusFromNetwork(network)
	if err != nil {
		return nil, err
	}

	secretNS := fmt.Sprintf("garden-%s", env)
	if env == "dev" {
		secretNS = fmt.Sprintf("garden-nightly")
	}
	secretName := fmt.Sprintf("%s.kubeconfig", status.ShootName)
	secret := &corev1.Secret{}

	err = client.Get(ctx, types.NamespacedName{Namespace: secretNS, Name: secretName}, secret)
	if err != nil {
		return nil, err
	}

	kcfg, err := clientcmd.RESTConfigFromKubeConfig(secret.Data["kubeconfig"])
	if err != nil {
		return nil, err
	}

	return kcfg, nil
}

func predNetwork(network cloudv1.Network, opt *config.DataplaneMatch) (bool, string) {
	supportedProviders := map[string]bool{
		cloudv1.ProviderTypeAWS: true,
		cloudv1.ProviderTypeGCP: true,
		cloudv1.ProviderTypeEKS: true,
		cloudv1.ProviderTypeGKE: true,
	}
	if _, exist := supportedProviders[network.Spec.Provider]; !exist {
		return false, fmt.Sprintf("provider %s is not supported", network.Spec.Provider)
	}

	unsupportedPhases := map[string]bool{
		cloudv1.NetworkPhaseDeleting:    true,
		cloudv1.NetworkPhaseHibernated:  true,
		cloudv1.NetworkPhaseHibernating: true,
		cloudv1.NetworkPhaseWakeUp:      true,
	}
	if _, exist := unsupportedPhases[network.Status.Phase]; exist {
		return false, fmt.Sprintf("network phase %s is not supported", network.Status.Phase)
	}

	if opt.Namespace != "" && opt.Namespace != network.Namespace {
		return false, fmt.Sprintf("namespace %s is not matched", network.Namespace)
	}

	if len(opt.FilterNamespacedNames) > 0 {
		exist := slices.ContainsFunc(opt.FilterNamespacedNames, func(i config.NamespacedName) bool {
			if i.Name == "" {
				return i.Namespace == network.Namespace
			}
			return i.Namespace == network.Namespace && i.Name == network.Name
		})
		if exist {
			return false, fmt.Sprintf("namespace %s is filtered", network.Namespace)
		}
	}

	return true, ""
}
