// Licensed Materials - Property of IBM
// (c) Copyright IBM Corporation 2018, 2019. All Rights Reserved.
// Note to U.S. Government Users Restricted Rights:
// Use, duplication or disclosure restricted by GSA ADP Schedule
// Contract with IBM Corp.
// Copyright (c) 2020 Red Hat, Inc.

package main

import (
	"context"
	"flag"
	"fmt"
	"os"
	"os/signal"
	"syscall"
	"time"

	"github.com/ghodss/yaml"
	"k8s.io/client-go/rest"

	v1 "k8s.io/api/core/v1"
	"k8s.io/apimachinery/pkg/api/errors"
	"k8s.io/apimachinery/pkg/fields"
	"k8s.io/client-go/tools/cache"

	"github.com/golang/glog"
	"github.com/open-cluster-management/hcm-compliance/pkg/common"
	"github.com/open-cluster-management/seed-sdk/pkg/client"
	"github.com/open-cluster-management/seed-sdk/pkg/controller"
	"github.com/open-cluster-management/seed-sdk/pkg/registry/clientsets"
	"github.com/open-cluster-management/seed-sdk/pkg/registry/config"
	"github.com/open-cluster-management/seed-sdk/pkg/registry/informers"
	"github.com/open-cluster-management/seed-sdk/pkg/scheme"

	policyApis "github.com/open-cluster-management/hcm-compliance/pkg/apis/policy/v1alpha1"
	placementRuleLister "github.com/open-cluster-management/hcm-compliance/pkg/client/listers/apps/v1"
	placement "github.com/open-cluster-management/multicloud-operators-foundation/pkg/apis/mcm/v1alpha1"
	placementLister "github.com/open-cluster-management/multicloud-operators-foundation/pkg/client/listers_generated/mcm/v1alpha1"
	placementrule "github.com/open-cluster-management/multicloud-operators-placementrule/pkg/apis/apps/v1"

	"github.com/open-cluster-management/hcm-compliance/pkg/propagator"

	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

	clregApis "github.com/open-cluster-management/hcm-compliance/pkg/apis/clusterregistry/v1alpha1"
	clreg "k8s.io/cluster-registry/pkg/apis/clusterregistry/v1alpha1"
	clregClientset "k8s.io/cluster-registry/pkg/client/clientset/versioned"
	clregInformer "k8s.io/cluster-registry/pkg/client/informers/externalversions"

	crdv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
	crdclientset "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"

	"github.com/open-cluster-management/multicloud-operators-foundation/pkg/utils/leaderelection"
)

// Kind of PlacementRule
const PlacementRuleKind = "PlacementRule"

// PlacementPoliciesResourcePlural as a plural name
const PlacementRuleResourcePlural = "placementrules"

// Kind of PlacementBindings
const PlacementBindingKind = "PlacementBinding"

// PlacementPoliciesResourcePlural as a plural name
const PlacementBindingResourcePlural = "placementbindings"

func main() {

	// TODO create a ConfigMap
	propagator := propagator.NewPropagator()
	kubeconfig := flag.String("kubeconfig", "", "Path to a kube config. Only required if out-of-cluster.")
	reconcileFrequency := flag.Duration("reconcileFrequency", time.Second*30, "time in seconds to reconcile, default is 30 seconds. example: \"30s\", or \"2h45m\". Valid time units are ms, s, m, h")
	tmpNamespace := flag.String("mcm-ns", common.HcmNamespace, `MCM "root" namespace`)
	leaderElect := flag.Bool("leaderElect", false, "Enable leader election")
	propagator.CheckedKind = policyApis.PolicyKind
	propagator.ReconcilePeriod = *reconcileFrequency
	propagator.LabelBlackList = []string{"applications.argoproj.io/app-name"}

	// TODO returned config, err := ParseFlags()
	config := config.ParseFlagsOrDie()
	defer glog.Flush()
	propagator.HcmNamespace = *tmpNamespace
	glog.V(3).Infof("Start Propagator with root MCM namespace as %s", propagator.HcmNamespace)

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

	// build config for the  cluster
	localConfig, err := common.BuildConfig(kubeconfig)
	if err != nil {
		glog.Error("BuildLocalConfig returned error:", err.Error())
		panic(err)
	}

	gscheme := scheme.NewScheme()
	policyApis.AddToScheme(gscheme.Scheme)
	// note: to add more types for the client, add more gscheme.SetPlural
	gscheme.SetPlural(policyApis.SchemeGroupVersion.WithKind(policyApis.PolicyKind), policyApis.PolicyResourcePlural)

	propagator.ResourceClient, err = client.New(localConfig, policyApis.SchemeGroupVersion, gscheme)
	if err != nil {
		glog.Error("new client creation returned error:", err.Error())
		panic(err)
	}

	run := func(stopCh <-chan struct{}) error {
		err = runPropagator(config, localConfig, propagator, reconcileFrequency)
		if err != nil {
			glog.Fatalf("Error run operator: %s", err.Error())
		}
		return nil
	}

	if err := leaderelection.Run(*leaderElect, propagator.ResourceClient.KubeClient, "kube-system", "policypropagator", make(chan struct{}), run); err != nil {
		glog.Fatalf("Error leaderelection run: %s", err.Error())
		panic(err)
	}
}

func runPropagator(config *rest.Config, localConfig *rest.Config, propagator *propagator.Propagator, reconcileFrequency *time.Duration) error {
	glog.Infof("I am leading...")
	// Install or upgrade Policy CRD
	err := checkOrInstallCRD(config, policyApis.PolicyResourcePlural+"."+policyApis.GroupName, policyApis.PolicyCRD)
	if err != nil {
		panic(err)
	}
	// Install Compliance CRD to backward support 3.2.0 klusterlet
	// err = checkOrInstallCRD(config, complianceApis.ComplianceResourcePlural+"."+complianceApis.GroupName, complianceApis.ComplianceCRD)
	// if err != nil {
	// 	panic(err)
	// }

	// Create Cluster Lister ===========================
	clientset := clientsets.AddClientsetForConfigOrDie("cluster",
		clregApis.CustomResourceDefinitions.ClientsetConstructor, config).(*clregClientset.Clientset)
	err = informers.AddFilteredFactory("default", clregApis.CustomResourceDefinitions.InformerFactoryConstructor,
		clientset, *reconcileFrequency, metav1.NamespaceAll, nil)
	if err != nil {
		panic(err)
	}

	factory := informers.Get("cluster").(clregInformer.SharedInformerFactory)
	l := factory.Clusterregistry().V1alpha1().Clusters().Lister()
	propagator.ClusterLister = &l
	clInformer := factory.Clusterregistry().V1alpha1().Clusters().Informer()

	ctx, cancelFunc := context.WithCancel(context.Background())
	defer cancelFunc()
	go clInformer.Run(ctx.Done())
	// End of Cluster Lister creation ====================

	// adding PlacementRule Lister ================
	scheme := scheme.NewScheme()
	scheme.SetPlural(placementrule.SchemeGroupVersion.WithKind(PlacementRuleKind), PlacementRuleResourcePlural)
	placementrule.AddToScheme(scheme.Scheme)

	prClient, err := client.New(config, placementrule.SchemeGroupVersion, scheme)
	if err != nil {
		panic(err)
	}

	ctx, prCancelFunc := context.WithCancel(context.Background())
	defer prCancelFunc()

	prInx, prCntr := cache.NewIndexerInformer(
		cache.NewListWatchFromClient(prClient.RESTClient, PlacementRuleResourcePlural, v1.NamespaceAll, fields.Everything()),
		&placementrule.PlacementRule{},
		time.Second*30,
		cache.ResourceEventHandlerFuncs{},
		cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc},
	)
	prLister := placementRuleLister.NewPlacementRuleLister(prInx)
	propagator.PRLister = &prLister
	go prCntr.Run(ctx.Done())
	//===================================================

	// adding PlacementBinding Lister ================
	scheme.SetPlural(placement.SchemeGroupVersion.WithKind(PlacementBindingKind), PlacementBindingResourcePlural)
	placement.AddToScheme(scheme.Scheme)

	pbClient, err := client.New(config, placement.SchemeGroupVersion, scheme)
	if err != nil {
		panic(err)
	}

	ctx, pbCancelFunc := context.WithCancel(context.Background())
	defer pbCancelFunc()

	pbInx, pbCntr := cache.NewIndexerInformer(
		cache.NewListWatchFromClient(pbClient.RESTClient, PlacementBindingResourcePlural, v1.NamespaceAll, fields.Everything()),
		&placement.PlacementBinding{},
		*reconcileFrequency,
		cache.ResourceEventHandlerFuncs{},
		cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc},
	)
	pbLister := placementLister.NewPlacementBindingLister(pbInx)
	propagator.PBLister = &pbLister
	go pbCntr.Run(ctx.Done())
	// ===================================================

	// Add Policy (Generic) Lister ===================
	inx, gnCntr := cache.NewIndexerInformer(
		cache.NewListWatchFromClient(propagator.ResourceClient.RESTClient, policyApis.PolicyResourcePlural, v1.NamespaceAll, fields.Everything()),
		&policyApis.Policy{},
		*reconcileFrequency,
		cache.ResourceEventHandlerFuncs{},
		cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc},
	)
	policyL := cache.NewGenericLister(inx, policyApis.Resource(policyApis.PolicyResourcePlural))
	gl := policyL.(cache.GenericLister)
	propagator.GenericLister = &gl
	go gnCntr.Run(ctx.Done())
	//===================================================

	if !cache.WaitForCacheSync(ctx.Done(), clInformer.HasSynced, gnCntr.HasSynced, prCntr.HasSynced, pbCntr.HasSynced) {
		glog.Infof("error waiting for informers to sync")
		return nil
	}
	glog.V(5).Infoln("All caches are synchronized")

	propagator.Recorder, err = common.CreateRecorder(propagator.ResourceClient.KubeClient, "policy-propagator")
	if err != nil {
		panic(err)
	}
	// start watchers
	// fix for https://zenhub.ibm.com/app/workspaces/icp-content-58eb8bceeaae55d225b7a0a7/issues/ibmprivatecloud/roadmap/20106
	// create cancel function to run remote controller
	ctx, cancelFn := context.WithCancel(context.Background())
	defer cancelFn()

	err = controller.New().
		ParseFlags().
		Watch("policies.v1alpha1.policy.mcm.ibm.com", policyApis.PolicyKind, &policyApis.SchemeBuilder).
		Reconcile(propagator.Reconcile).
		Finalize(propagator.Finalizer).
		ResyncPeriod(*reconcileFrequency).
		//Install(compApis.CustomResourceDefinitions).
		RunWithContext(ctx)
	if err != nil {
		panic(err)
	}

	// Configure cluster-registry & placement bind controller
	err = controller.New().
		ParseFlags().
		Watch("clusters.v1alpha1.clusterregistry.k8s.io", "Cluster", &clreg.SchemeBuilder).
		From(localConfig).
		Reconcile(propagator.ReconcileCluster).
		Finalize(propagator.FinalizerCluster).
		ResyncPeriod(*reconcileFrequency).
		Watch("placementbindings.v1alpha1.mcm.ibm.com", PlacementBindingKind, &placement.SchemeBuilder).
		From(localConfig).
		// Namespace(propagator.HcmNamespace).
		Reconcile(propagator.ReconcileBinding).
		Finalize(propagator.FinalizerBinding).
		ResyncPeriod(*reconcileFrequency).
		Run()
	if err != nil {
		panic(err)
	}
	return nil
}

// some clean up logic can be added here
func cleanup() {
	fmt.Println("cleaning up...")
	os.Exit(0)
}

func checkOrInstallCRD(config *rest.Config, crdName string, crdData string) error {

	crdClient, err := crdclientset.NewForConfig(config)
	if err != nil {
		glog.Fatalf("Error building CRD clientset: %s", err.Error())
		return err
	}
	existingCRD, err1 := crdClient.ApiextensionsV1beta1().CustomResourceDefinitions().Get(crdName, metav1.GetOptions{})
	if errors.IsNotFound(err1) {
		glog.Infof("Installing CRD %s", crdName)

		// Install CRD
		crd := new(crdv1beta1.CustomResourceDefinition)
		err = yaml.Unmarshal([]byte(crdData), crd)
		if err != nil {
			glog.Fatal("Unmarshal crd ", err.Error(), "\n", crdData)
			return err
		}

		glog.V(10).Info("Loaded  CRD: ", crd, "\n - From - \n", crdData)
		_, err = crdClient.ApiextensionsV1beta1().CustomResourceDefinitions().Create(crd)
		if err != nil {
			glog.Fatal("Creating CRD", err.Error())
			return err
		}
	} else if crdName == "policies.policy.mcm.ibm.com" { // upgrade policy CRD if exists
		glog.Infof("Updating CRD %s", crdName)

		// Install CRD
		crd := new(crdv1beta1.CustomResourceDefinition)
		err = yaml.Unmarshal([]byte(crdData), crd)
		if err != nil {
			glog.Fatal("Unmarshal crd ", err.Error(), "\n", crdData)
			return err
		}
		crd.SetResourceVersion(existingCRD.GetResourceVersion())
		_, err = crdClient.ApiextensionsV1beta1().CustomResourceDefinitions().Update(crd)
		if err != nil {
			glog.Fatal("Updating CRD", err.Error())
			return err
		}
	}
	return err
}
