package app

import (
	"context"
	"flag"
	"fmt"
	"os"
	"strings"

	awsconfig "github.com/aws/aws-sdk-go-v2/config"
	"github.com/go-logr/logr"
	"github.com/spf13/cobra"
	"github.com/spf13/pflag"
	configsdk "github.com/tidbcloud/config-service/sdk"
	cloudv1 "github.com/tidbcloud/infra-api/api/v1"
	"github.com/tidbcloud/infra-api/controllers/backup"
	cloudImports "github.com/tidbcloud/infra-api/controllers/imports"
	"github.com/tidbcloud/infra-api/pkg/provider"
	"github.com/tidbcloud/infra-api/pkg/providers/aws"
	"go.uber.org/zap"
	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/fields"
	ctrl "sigs.k8s.io/controller-runtime"
	"sigs.k8s.io/controller-runtime/pkg/cache"
	"sigs.k8s.io/controller-runtime/pkg/client"
	ctrlcluster "sigs.k8s.io/controller-runtime/pkg/cluster"
	"sigs.k8s.io/controller-runtime/pkg/healthz"
	zapopt "sigs.k8s.io/controller-runtime/pkg/log/zap"
	"sigs.k8s.io/controller-runtime/pkg/predicate"

	"github.com/tidbcloud/shared-provider/cmd/manager/app/config"
	"github.com/tidbcloud/shared-provider/cmd/manager/app/options"
	"github.com/tidbcloud/shared-provider/pkg/controllers/cluster"
	"github.com/tidbcloud/shared-provider/pkg/controllers/imports"
	"github.com/tidbcloud/shared-provider/pkg/controllers/network"
	"github.com/tidbcloud/shared-provider/pkg/controllers/networkpolicy"
	"github.com/tidbcloud/shared-provider/pkg/controllers/pod"
	"github.com/tidbcloud/shared-provider/pkg/controllers/s3clean"
	"github.com/tidbcloud/shared-provider/pkg/gitops"
	"github.com/tidbcloud/shared-provider/pkg/manager"
	"github.com/tidbcloud/shared-provider/pkg/scheme"
	"github.com/tidbcloud/shared-provider/pkg/types"
	"github.com/tidbcloud/shared-provider/pkg/utils"
	"github.com/tidbcloud/shared-provider/pkg/utils/labels"
	"github.com/tidbcloud/shared-provider/pkg/version"
)

// NewCommand returns app command.
func NewCommand() *cobra.Command {
	setupLog := ctrl.Log.WithName("setup")

	opts := options.New()
	lvl := zap.NewAtomicLevelAt(zap.FatalLevel)
	zapOpts := zapopt.Options{
		Development:     true,
		StacktraceLevel: &lvl,
	}

	cmd := &cobra.Command{
		Use:  "manager",
		Long: "manager watches CRD and implements infra API",
		Run: func(cmd *cobra.Command, _ []string) {
			fmt.Printf("%v\n", version.Get())

			ctrl.SetLogger(zapopt.New(zapopt.UseFlagOptions(&zapOpts)))
			printFlags(setupLog, cmd.Flags())

			cfg, err := opts.Config()
			if err != nil {
				setupLog.Error(err, "can't parse options to config")
				os.Exit(-1)
			}

			setupLog.Info("begin to start manager")
			if err := Run(cfg); err != nil {
				setupLog.Error(err, "run manager failed")
				os.Exit(-1)
			}
		},
	}
	opts.AddFlags(cmd.Flags())
	zapOpts.BindFlags(flag.CommandLine)

	cmd.AddCommand(NewVersionCmd())

	return cmd
}

// NewVersionCmd return cmd reports version.
func NewVersionCmd() *cobra.Command {
	cmd := &cobra.Command{
		Use:  "version",
		Long: "manager version",
		Run: func(_ *cobra.Command, _ []string) {
			fmt.Printf("%v\n", version.Get())
		},
	}

	return cmd
}

// Run runs the operator.
// nolint: funlen // need more funlen
func Run(cfg *config.Config) error {
	mgr, err := newManager(cfg)
	if err != nil {
		return err
	}
	cachedClient, err := client.NewDelegatingClient(client.NewDelegatingClientInput{
		CacheReader:     mgr.GetCache(),
		Client:          mgr.GetClient(),
		UncachedObjects: nil,
	})
	if err != nil {
		return err
	}

	// configService is an interface of gitops which provides the configuration of TiDB clusters
	configService := configsdk.New(cfg.ConfigServiceUrl)

	// This assume we can only run with one provider(one account for aws type)
	awsCfg, err := awsconfig.LoadDefaultConfig(context.TODO())
	if err != nil {
		return fmt.Errorf("load aws configuration err: %w", err)
	}
	awsClient := aws.NewClient(awsCfg)

	if err = newNetworkReconciler(cfg, awsClient).SetupWithManager(mgr); err != nil {
		return fmt.Errorf("unable to create Network controller: %w", err)
	}

	if err = (&cluster.Reconciler{
		AwsClient:               awsClient,
		ConfigService:           configService,
		BaseProxy:               cfg.BaseProxy,
		PublicHostedZoneDomain:  cfg.PublicHostedZoneDomain,
		PublicHostedZoneId:      cfg.PublicHostedZoneId,
		KubeClusterName:         cfg.KubeClusterName,
		LoadBalancerClusterName: cfg.LoadBalancerClusterName,
		RemoteWriteUrl:          cfg.RemoteWriteUrl,
		EvictDuration:           cfg.EvictPodDuration,
		ConfigProvider:          gitops.NewProviderConfigProvider(),
		Env:                     getEnvFromKubeClusterName(cfg.KubeClusterName),
		AWSRegion:               cfg.AWSRegion,
	}).SetupWithManager(mgr); err != nil {
		return fmt.Errorf("unable to create Cluster controller: %w", err)
	}

	if err = (&networkpolicy.Reconciler{
		Client:            cachedClient,
		KubeClusterName:   cfg.KubeClusterName,
		LoadBalancerCIDRs: cfg.LoadBalancerCIDRs,
	}).SetupWithManager(mgr); err != nil {
		return fmt.Errorf("unable to create NetworkPolicy controller: %w", err)
	}

	if cfg.EvictPodDuration > 0 {
		if err = (&pod.Reconciler{
			KubeClusterName: cfg.KubeClusterName,
			EvictDuration:   cfg.EvictPodDuration,
		}).SetupWithManager(mgr); err != nil {
			return fmt.Errorf("unable to create Pod controller: %w", err)
		}
	}

	if err = (&s3clean.Reconciler{
		AwsClient:       awsClient,
		KubeClusterName: cfg.KubeClusterName,
	}).SetupWithManager(mgr); err != nil {
		return fmt.Errorf("unable to create s3 clean controller: %w", err)
	}

	ptp := utils.GetPredicateForProviderTypes(types.ProviderTypeGCPSharedTier, types.ProviderTypeAWSSharedTier)

	// predicate for backup, restore and import
	briPredicate, err := predicate.LabelSelectorPredicate(metav1.LabelSelector{
		MatchLabels: map[string]string{
			labels.LabelKeyKubernetesCluster: cfg.KubeClusterName,
		},
	})
	if err != nil {
		return fmt.Errorf("unable to create label selector predicate for provider: %w", err)
	}

	infraMgr := mgr.(*manager.ControllerManager).InfraMgr() // mgr for infra API server
	clusterCli := mgr.GetClient()                           // client for the shared EKS cluster
	if err = backup.NewBackupReconciler(infraMgr, ctrl.Log.WithName("controllers").WithName("Backup"),
		nil, clusterCli, configService, provider.NewUnimplemented()).
		SetupWithManager(infraMgr, ptp, briPredicate); err != nil {
		return fmt.Errorf("unable to create Backup controller: %w", err)
	}
	if err = backup.NewRestoreReconciler(infraMgr, ctrl.Log.WithName("controllers").WithName("Restore"),
		nil, clusterCli, configService, nil, nil, provider.NewUnimplemented()).
		SetupWithManager(infraMgr, ptp, briPredicate); err != nil {
		return fmt.Errorf("unable to create Restore controller: %w", err)
	}
	if err = cloudImports.NewImportReconciler(
		infraMgr, ctrl.Log.WithName("controllers").WithName("Import"), nil,
		clusterCli, &imports.ImportEnv{
			Env:    getEnvFromKubeClusterName(cfg.KubeClusterName),
			Client: clusterCli,
		}, configService).SetUpGitopsV2().
		SetupWithManager(infraMgr, ptp, briPredicate); err != nil {
		return fmt.Errorf("unable to create Import controller: %w", err)
	}

	if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil {
		return fmt.Errorf("unable to set up health check: %w", err)
	}
	if err := mgr.AddReadyzCheck("readyz", healthz.Ping); err != nil {
		return fmt.Errorf("unable to set up ready check: %w", err)
	}

	if err = addInfraIndexFields(mgr.InfraCluster()); err != nil {
		return err
	}

	if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil {
		return fmt.Errorf("problem running manager: %w", err)
	}

	return nil
}

func newNetworkReconciler(cfg *config.Config, awsClient aws.Client) *network.Reconciler {
	return &network.Reconciler{
		AddonConfigMapNamespace: cfg.AddonConfigMapNamespace,
		AddonConfigMapName:      cfg.AddonConfigMapName,
		KubeClusterName:         cfg.KubeClusterName,
		Env:                     getEnvFromKubeClusterName(cfg.KubeClusterName),
		AWSAccountID:            cfg.AWSAccountID,
		ImportRoleARN:           cfg.ImportRoleARN,
		OIDCProviderARN:         cfg.OIDCProviderARN,
		AwsClient:               awsClient,
		GCPProjectID:            cfg.GCPProjectID,
	}
}

func newManager(cfg *config.Config) (manager.Manager, error) {
	mgr, err := manager.New(cfg.Kubeconfig, cfg.InfraKubeconfig, manager.Options{
		Manager: ctrl.Options{
			Scheme:                 scheme.Scheme,
			MetricsBindAddress:     cfg.MetricsBindAddress,
			Port:                   cfg.WebhookPort,
			HealthProbeBindAddress: cfg.HealthProbeBindAddress,
			LeaderElection:         cfg.EnableLeaderElection,
			LeaderElectionID:       "7dd07112.pingcap.com",
			// NewClient use a direct client which never use cache
			NewClient: manager.NewDirectClient,
			NewCache: cache.BuilderWithOptions(cache.Options{
				SelectorsByObject: cache.SelectorsByObject{
					&corev1.Endpoints{}: {
						Field: fields.SelectorFromSet(
							fields.Set{
								"metadata.name":      types.KubernetesEndpointName,
								"metadata.namespace": corev1.NamespaceDefault,
							},
						),
					},
				},
			}),
		},
		InfraCluster: ctrlcluster.Options{
			Scheme: scheme.Scheme,
		},
	})
	if err != nil {
		return nil, fmt.Errorf("unable to create manager: %w", err)
	}

	return mgr, nil
}

func getEnvFromKubeClusterName(cn string) string {
	// nolint: gomnd // TODO: replaced by Cut in the future
	ss := strings.SplitN(cn, "-", 2)
	// invalid format, treat it as dev env
	if len(ss) == 1 {
		return types.BillingTagValEnvironmentDev
	}
	switch ss[0] {
	case types.BillingTagValEnvironmentDev,
		types.BillingTagValEnvironmentStaging,
		types.BillingTagValEnvironmentProd:

		return ss[0]
	default:
		return types.BillingTagValEnvironmentDev
	}
}

func addInfraIndexFields(infraCluster ctrlcluster.Cluster) error {
	// add some index fields for the cluster controller.
	if err := infraCluster.GetFieldIndexer().IndexField(context.TODO(), &cloudv1.Backup{},
		"spec.clusterName", func(obj client.Object) []string {
			backup := obj.(*cloudv1.Backup)
			return []string{backup.Spec.ClusterName}
		}); err != nil {
		return fmt.Errorf("unable to add index for Backup spec.ClusterName: %w", err)
	}
	if err := infraCluster.GetFieldIndexer().IndexField(context.TODO(), &cloudv1.Restore{},
		"spec.clusterName", func(obj client.Object) []string {
			restore := obj.(*cloudv1.Restore)
			return []string{restore.Spec.ClusterName}
		}); err != nil {
		return fmt.Errorf("unable to add index for Restore spec.ClusterName: %w", err)
	}
	if err := infraCluster.GetFieldIndexer().IndexField(context.TODO(), &cloudv1.Import{},
		"spec.clusterName", func(obj client.Object) []string {
			importObj := obj.(*cloudv1.Import)
			return []string{importObj.Spec.ClusterName}
		}); err != nil {
		return fmt.Errorf("unable to add index for Import spec.ClusterName: %w", err)
	}
	return nil
}

func printFlags(logger logr.Logger, fs *pflag.FlagSet) {
	fs.VisitAll(func(f *pflag.Flag) {
		logger.Info("FLAG", f.Name, f.Value)
	})
}
