package main

import (
	"context"
	"flag"
	"fmt"
	"os"
	"os/signal"
	"path/filepath"
	"syscall"

	"github.com/spf13/pflag"
	"go.uber.org/zap"
	appsv1 "k8s.io/api/apps/v1"
	corev1 "k8s.io/api/core/v1"
	rbacv1 "k8s.io/api/rbac/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/client-go/rest"
	"k8s.io/client-go/tools/clientcmd"
	ctrl "sigs.k8s.io/controller-runtime"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/envtest"
	logzap "sigs.k8s.io/controller-runtime/pkg/log/zap"

	cloudv1 "github.com/tidbcloud/infra-api/api/v1"
	"github.com/tidbcloud/infra-provider/pkg/dataplane"
	"github.com/tidbcloud/infra-provider/pkg/scheme"
	"github.com/tidbcloud/infra-provider/pkg/util"
)

var (
	attachControlPlaneOut = pflag.Bool("debug-env", false, "attach to test env (apiserver & etcd) output -- just a convinience flag to force KUBEBUILDER_ATTACH_CONTROL_PLANE_OUTPUT=true")

	infraConfigPath          = pflag.String("infra-config", "", "infra config path")
	clusterServiceConfigPath = pflag.String("cluster-service-config", "", "kubeconfig path of cluster service")
	cluster                  = pflag.StringP("cluster", "c", "", "cluster id")
	namespace                = pflag.StringP("namespace", "n", "", "namespace")

	awsDataplaneRoleName = pflag.String("aws-dataplane-role-name", "assumed-readonly-cluster-controller", "dataplane role name for fetching dataplane data")

	checkerCMDPath      = pflag.String("checker-cmd-path", "eks-provider", "checker cmd path")
	checkerOnlineConfig = pflag.Bool("checker-online-config", false, "read configs from online cluster service namespace")
	checkerConfigPath   = pflag.String("checker-config-path", "eks-config.yaml", "checker config path")
)

// have a separate function so we can return an exit code w/o skipping defers
func runMain() int {
	loggerOpts := &logzap.Options{
		Development: true, // a sane default
		ZapOpts:     []zap.Option{zap.AddCaller()},
	}
	{
		var goFlagSet flag.FlagSet
		loggerOpts.BindFlags(&goFlagSet)
		pflag.CommandLine.AddGoFlagSet(&goFlagSet)
	}
	pflag.Parse()
	ctrl.SetLogger(logzap.New(logzap.UseFlagOptions(loggerOpts)))
	ctrl.Log.Info("Starting...")

	log := ctrl.Log.WithName("main")

	if *cluster == "" {
		log.Info("cluster is not set")
		return 1
	}

	infra, err := newClientFromFile(*infraConfigPath, "")
	if err != nil {
		log.Error(err, "cannot new client for infra api", "config", *infraConfigPath)
		return 1
	}

	c := cloudv1.Cluster{}
	if err := infra.Get(context.Background(), client.ObjectKey{
		Namespace: *namespace,
		Name:      *cluster,
	}, &c); err != nil {
		log.Error(err, "cannot get cluster", "cluster", *cluster, "namespace", *namespace)
		return 1
	}

	switch c.GetProviderType() {
	case cloudv1.ProviderTypeEKS:
		region := os.Getenv("AWS_REGION")
		if region != "" && region != c.GetRegion() {

			log.Info("region is mismatched", "cluster region", c.GetRegion(), "seed region", region)
			return 1
		}
	case cloudv1.ProviderTypeGKE:
	default:
		log.Error(fmt.Errorf("unexpected provider type: %v", c.GetProviderType()), "unexpected provider type")
		return 1
	}

	if *checkerOnlineConfig {
		clusterService, err := newClientFromFile(*clusterServiceConfigPath, "")
		if err != nil {
			log.Error(err, "cannot new client for infra api", "config", *infraConfigPath)
			return 1
		}
		if err := persistCheckerConfigs(context.Background(), clusterService); err != nil {
			log.Error(err, "cannot persist checker config")
			return 1
		}
		*checkerConfigPath = "/etc/cluster-controller/main/root-config.yaml"
	}

	from, err := newClientFromCluster(&c, *awsDataplaneRoleName, "")
	if err != nil {
		log.Error(err, "cannot new client", "cluster", *cluster, "namespace", *namespace)
		return 1
	}

	env := &envtest.Environment{}

	if *attachControlPlaneOut {
		os.Setenv("KUBEBUILDER_ATTACH_CONTROL_PLANE_OUTPUT", "true")
	}

	log.Info("Starting apiserver & etcd")
	cfg, err := env.Start()
	if err != nil {
		log.Error(err, "unable to start the test environment")
		// shut down the environment in case we started it and failed while
		// installing CRDs or provisioning users.
		if err := env.Stop(); err != nil {
			log.Error(err, "unable to stop the test environment after an error (this might be expected, but just though you should know)")
		}
		return 1
	}

	log.Info("apiserver running", "host", cfg.Host)
	defer func() {
		log.Info("Shutting down apiserver & etcd")
		err = env.Stop()
		if err != nil {
			log.Error(err, "unable to stop the test environment")
			return
		}

		log.Info("Shutdown successful")
	}()

	// NB(directxman12): this group is unfortunately named, but various
	// kubernetes versions require us to use it to get "admin" access.
	admin, err := env.ControlPlane.AddUser(envtest.User{
		Name:   "envtest-admin",
		Groups: []string{"system:masters"},
	}, nil)
	if err != nil {
		log.Error(err, "unable to provision admin user, continuing on without it")
		return 1
	}

	adminKubeconfigFile, err := os.CreateTemp("", "dataplane-kubeconfig-")
	if err != nil {
		log.Error(err, "unable to create kubeconfig file, continuing on without it")
		return 1
	}
	defer os.Remove(adminKubeconfigFile.Name())

	{
		log := log.WithValues("path", adminKubeconfigFile.Name())
		log.V(1).Info("Writing kubeconfig")

		kubeConfig, err := admin.KubeConfig()
		if err != nil {
			log.Error(err, "unable to create kubeconfig")
			return 1
		}

		if _, err := adminKubeconfigFile.Write(kubeConfig); err != nil {
			log.Error(err, "unable to save kubeconfig")
			return 1
		}

		log.Info("Wrote kubeconfig")
	}

	to, err := newClientFromFile(adminKubeconfigFile.Name(), "")
	if err != nil {
		log.Error(err, "cannot new client to", "config", adminKubeconfigFile.Name())
		return 1
	}

	if opts := env.WebhookInstallOptions; opts.LocalServingPort != 0 {
		log.Info("webhooks configured for", "host", opts.LocalServingHost, "port", opts.LocalServingPort, "dir", opts.LocalServingCertDir)
	}

	ctx, cancel := context.WithCancel(context.Background())

	s := make(chan os.Signal, 1)
	signal.Notify(s, os.Interrupt, syscall.SIGTERM)
	go func() {
		<-s
		cancel()
	}()

	ns := "tidb" + *cluster
	h := NewTransfer(ns, from, to)
	if err := h.Transfer(ctx); err != nil {
		log.Error(err, "transfer failed")
		return 1
	}

	readonly, err := env.ControlPlane.AddUser(envtest.User{
		Name:   "readonly-cluster-controller",
		Groups: []string{"readonly-cluster-controller"},
	}, nil)
	if err != nil {
		log.Error(err, "unable to provision read only cluster controller")
		return 1
	}

	readonlyKubeconfigFile, err := os.CreateTemp("", "dataplane-kubeconfig-")
	if err != nil {
		log.Error(err, "unable to create kubeconfig file")
		return 1
	}
	defer os.Remove(readonlyKubeconfigFile.Name())

	kubeConfig, err := readonly.KubeConfig()
	if err != nil {
		log.Error(err, "unable to create kubeconfig")
		return 1
	}

	if _, err := readonlyKubeconfigFile.Write(kubeConfig); err != nil {
		log.Error(err, "unable to save kubeconfig")
		return 1
	}

	if err := to.Create(ctx, &rbacv1.ClusterRole{
		ObjectMeta: metav1.ObjectMeta{
			Name: "readonly-cluster-controller",
		},
		Rules: []rbacv1.PolicyRule{
			{
				APIGroups: []string{"*"},
				Resources: []string{"*"},
				Verbs:     []string{"get", "list", "watch"},
			},
		},
	}); err != nil {
		log.Error(err, "unable to create cluster role readonly cluster controller")
		return 1
	}

	if err := to.Create(ctx, &rbacv1.ClusterRoleBinding{
		ObjectMeta: metav1.ObjectMeta{
			Name: "readonly-cluster-controller",
		},
		RoleRef: rbacv1.RoleRef{
			APIGroup: rbacv1.SchemeGroupVersion.Group,
			Kind:     "ClusterRole",
			Name:     "readonly-cluster-controller",
		},
		Subjects: []rbacv1.Subject{
			{
				APIGroup: rbacv1.SchemeGroupVersion.Group,
				Kind:     rbacv1.GroupKind,
				Name:     "readonly-cluster-controller",
			},
		},
	}); err != nil {
		log.Error(err, "unable to create cluster role binding readonly cluster controller")
		return 1
	}

	log.WithValues("path", readonlyKubeconfigFile.Name()).Info("ready for test")

	checker := NewChecker(*checkerCMDPath, *checkerConfigPath, *infraConfigPath, readonlyKubeconfigFile.Name(), *cluster, *awsDataplaneRoleName, c.GetProviderType(), c.GetRegion())
	if err := checker.Check(ctx); err != nil {
		log.Error(err, "run checker failed")
		return 1
	}

	<-ctx.Done()

	return 0
}

func newClientFromFile(configPath, context string) (client.Client, error) {
	if configPath == "" {
		cfg, err := rest.InClusterConfig()
		if err != nil {
			return nil, fmt.Errorf("can't parse in cluster kubeconfig: %w", err)
		}
		return client.New(cfg, client.Options{
			Scheme: scheme.Scheme,
		})
	}
	rule := clientcmd.NewDefaultClientConfigLoadingRules()
	rule.ExplicitPath = configPath
	kubeconfig, err := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(
		rule,
		&clientcmd.ConfigOverrides{CurrentContext: context}).ClientConfig()
	if err != nil {
		return nil, fmt.Errorf("can't parse kubeconfig: %w", err)
	}

	return client.New(kubeconfig, client.Options{
		Scheme: scheme.Scheme,
	})
}

func newClientFromCluster(c *cloudv1.Cluster, awsRoleName string, externalID string) (client.Client, error) {
	switch c.GetProviderType() {
	case cloudv1.ProviderTypeEKS:
		roleARN := util.GetRoleARN(c.Spec.Kubernetes.Account, awsRoleName)
		cfg, err := dataplane.NewRESTConfigForEKS(c.Spec.Kubernetes.Name, c.Spec.Kubernetes.Endpoint, c.GetRegion(), roleARN, externalID, c.Spec.Kubernetes.CAData)
		if err != nil {
			return nil, fmt.Errorf("can't new kubeconfig for eks: %w", err)
		}
		return client.New(cfg, client.Options{
			Scheme: scheme.Scheme,
		})
	case cloudv1.ProviderTypeGKE:
		cfg, err := dataplane.NewRESTConfigForGKE(c.Spec.Kubernetes.Name, c.Spec.Kubernetes.Endpoint, c.Spec.Kubernetes.CAData)
		if err != nil {
			return nil, fmt.Errorf("can't new kubeconfig for gke: %w", err)
		}
		return client.New(cfg, client.Options{
			Scheme: scheme.Scheme,
		})
	}

	return nil, fmt.Errorf("unsupported provider type: %v", c.GetProviderType())
}

func persistCheckerConfigs(ctx context.Context, c client.Client) error {
	deploy := appsv1.Deployment{
		ObjectMeta: metav1.ObjectMeta{
			Namespace: "cluster-service",
			Name:      "cluster-controller",
		},
	}
	if err := c.Get(ctx, client.ObjectKeyFromObject(&deploy), &deploy); err != nil {
		return err
	}

	for _, vol := range deploy.Spec.Template.Spec.Volumes {
		if vol.ConfigMap == nil {
			continue
		}

		name := vol.ConfigMap.Name
		cm := corev1.ConfigMap{
			ObjectMeta: metav1.ObjectMeta{
				Namespace: "cluster-service",
				Name:      name,
			},
		}
		if err := c.Get(ctx, client.ObjectKeyFromObject(&cm), &cm); err != nil {
			return err
		}
		prefix := "/etc/cluster-controller"
		switch vol.Name {
		case "root-config":
			prefix = filepath.Join(prefix, "main")
		case "extra-config":
			prefix = filepath.Join(prefix, "extra")
		}
		if err := persistConfigMap(ctx, &cm, prefix); err != nil {
			return err
		}
	}

	return nil
}

func persistConfigMap(_ context.Context, cm *corev1.ConfigMap, path string) error {
	if err := os.MkdirAll(path, 0644); err != nil {
		return fmt.Errorf("cannot mkdir %v: %w", path, err)
	}
	for k, v := range cm.Data {
		f := filepath.Join(path, k)
		if err := os.WriteFile(f, []byte(v), 0644); err != nil {
			return fmt.Errorf("cannot write file %v: %w", f, err)
		}
	}
	return nil
}

func main() {
	os.Exit(runMain())
}
