// 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 sync

import (
	"fmt"
	"strings"

	"github.com/golang/glog"
	"k8s.io/apimachinery/pkg/api/equality"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/client-go/tools/record"

	"github.com/open-cluster-management/hcm-compliance/pkg/common"
	seedclient "github.com/open-cluster-management/seed-sdk/pkg/client"
	"github.com/open-cluster-management/seed-sdk/pkg/context"
	resv1 "github.com/open-cluster-management/seed-sdk/pkg/types/apis/resource/v1"
	"github.com/open-cluster-management/seed-sdk/pkg/util"
)

//
// Generically handle object spec and status reconciliation and finalization on runtime.Object types
//

const Name = "sync"

var myFinalizerNames = []string{Name + common.Finalizer, common.OldFinalizer}

// reconcile Spec from hub cluster to managed cluster
func specReconcile(ctx context.Context, remoteObj runtime.Object, hubClient, managedClient *seedclient.ResourceClient) error {
	return specReconcileEvent(ctx, remoteObj, hubClient, managedClient, nil)
}

// reconcile Spec from hub cluster to managed cluster
func specReconcileEvent(ctx context.Context, remoteObj runtime.Object, hubClient, managedClient *seedclient.ResourceClient, recorder record.EventRecorder) error {
	kind := remoteObj.GetObjectKind().GroupVersionKind().Kind
	meta := remoteObj.(metav1.ObjectMetaAccessor).GetObjectMeta()

	glog.V(5).Infof("%s %s updated in hub cluster", kind, meta.GetSelfLink())
	// adding sync finalizer on policy object is causing policy object not removable on hub
	// do not add sync finalizer on Policy target object
	if kind != "Policy" {
		resv1.EnsureFinalizerAndPut(ctx, remoteObj, Name+common.Finalizer)
	}
	localObj := (remoteObj).DeepCopyObject()
	// remove finalizers from local object
	lmeta := localObj.(metav1.ObjectMetaAccessor).GetObjectMeta()
	lmeta.SetFinalizers([]string{})

	//update annotations
	if !equality.Semantic.DeepEqual(lmeta.GetAnnotations(), meta.GetAnnotations()) {
		glog.V(5).Infof("Updating annotations for local object %s...", lmeta.GetName())
		lmeta.SetAnnotations(meta.GetAnnotations())
	}

	err := managedClient.Get(localObj) // check if object exists in local cluster
	if err != nil {
		glog.V(5).Infof("replicating %s %s to managed cluster", kind, meta.GetName())
		err = createReplicatedObject(localObj, managedClient) // create replica object in local cluster
		if err != nil {
			glog.Errorf("error replicating %s: %s", kind, err)
			if recorder != nil {
				recorder.Event(localObj, "Normal", "PolicyReplicated", fmt.Sprintf("Policy %s creation error in %s, returned error: %s", meta.GetName(), meta.GetNamespace(), err))
			}
			return err
		}
		if recorder != nil {
			recorder.Event(localObj, "Normal", "PolicyReplicated", fmt.Sprintf("Policy %s created in %s", meta.GetName(), meta.GetNamespace()))
		}
		return nil
	}
	if kind == "Policy" {
		localObj = common.RemoveStatusInSpec(localObj)
	}
	glog.V(5).Infof("ensuring %s %s Spec in managed cluster matches hub Spec", kind, meta.GetName())
	var updated bool
	updated, err = updateField("Spec", remoteObj, localObj, managedClient)
	if err != nil {
		if recorder != nil {
			recorder.Event(localObj, "Warning", "PolicySpecSynced", fmt.Sprintf("Policy %s spec sync failed in %s, returned error: %s", meta.GetName(), meta.GetNamespace(), err))
		}
		return err
	}
	if updated && recorder != nil {
		recorder.Event(localObj, "Normal", "PolicySpecSynced", fmt.Sprintf("Policy %s spec synced in %s", meta.GetName(), meta.GetNamespace()))
	}
	return nil
}

// finalize hub object
func specFinalize(ctx context.Context, remoteObj runtime.Object, hubClient, managedClient *seedclient.ResourceClient) error {
	kind := remoteObj.GetObjectKind().GroupVersionKind().Kind
	meta := remoteObj.(metav1.ObjectMetaAccessor).GetObjectMeta()

	glog.V(5).Infof("%s %s deleted in hub cluster", kind, meta.GetSelfLink())

	localObj := remoteObj.DeepCopyObject()
	err := managedClient.Get(localObj) // check if object exists in local cluster
	if err != nil {
		glog.V(5).Infof("%s %s not found in managed cluster, nothing to be done", kind, meta.GetName())
		return nil
	}
	glog.V(5).Infof("deleting %s %s from managed cluster", kind, meta.GetName())
	err = managedClient.Delete(localObj, &metav1.DeleteOptions{})
	if err != nil {
		return fmt.Errorf("error deleting %s: %s", kind, err)
	}
	//resv1.RemoveFinalizerAndPut(ctx, remoteObj, Name+common.Finalizer)
	common.RemoveFinalizerAndPut(ctx, remoteObj, myFinalizerNames)
	return nil
}

func statusReconcile(ctx context.Context, localObj runtime.Object, hubClient, managedClient *seedclient.ResourceClient) error {
	return statusReconcileEvent(ctx, localObj, hubClient, managedClient, nil)
}

// reconcile status from managed cluster
func statusReconcileEvent(ctx context.Context, localObj runtime.Object, hubClient, managedClient *seedclient.ResourceClient, recorder record.EventRecorder) error {
	kind := localObj.GetObjectKind().GroupVersionKind().Kind
	meta := localObj.(metav1.ObjectMetaAccessor).GetObjectMeta()

	glog.V(5).Infof("%s %s updated in managed cluster", kind, meta.GetSelfLink())
	resv1.EnsureFinalizerAndPut(ctx, localObj, Name+common.Finalizer)
	// check if object exists in remote cluster
	remoteObj, err := hubClient.GetObject(kind, meta.GetNamespace(), meta.GetName())
	if err != nil {
		glog.V(5).Infof("error looking up %s %s in hub cluster: %s", kind, meta.GetName(), err)
		// distinguish the case where there is no communication with remote cluster
		// or remote cluster is down from case where there is communication but object is not found
		// in the latter case the object should be deleted from local.
		if strings.Contains(err.Error(), "not found") {
			glog.V(5).Infof("%s %s might have been deleted in hub cluster", kind, meta.GetSelfLink())
			glog.V(5).Infof("deleting %s %s from managed cluster", kind, meta.GetName())
			err = managedClient.Delete(localObj, &metav1.DeleteOptions{})
			if err != nil {
				return fmt.Errorf("error deleting %s: %s", kind, err)
			}
		}

	} else { // object exists in local cluster. If there is a different set of specs, update the specs
		glog.V(5).Infof("ensuring %s %s Status matches in hub and managed clusters", kind, meta.GetName())
		var updated bool
		updated, err = updateField("Status", localObj, remoteObj, hubClient)
		if err != nil {
			if recorder != nil {
				recorder.Event(remoteObj, "Warning", "PolicyStatusSynced", fmt.Sprintf("Policy %s status sync failed in %s, returned error: %s", meta.GetName(), meta.GetNamespace(), err))
			}
			return err
		}
		if updated && recorder != nil {
			recorder.Event(remoteObj, "Normal", "PolicyStatusSynced", fmt.Sprintf("Policy %s status synced in %s", meta.GetName(), meta.GetNamespace()))
		}
		// we do not want to allow changing local spec on local cluster. This should
		// only be done from remote cluster. To enforce this, we sync spec too in case a change
		// was done on local cluster
		// excpet for policy
		// if kind != "Policy" {
		// 	_ = updateField("Spec", remoteObj, localObj, managedClient)
		// }

		// if err != nil {
		// 	return err
		// }
	}
	return nil
}

// finalize object in managed cluster
func statusFinalize(ctx context.Context, localObj runtime.Object, _, _ *seedclient.ResourceClient) error {
	kind := localObj.GetObjectKind().GroupVersionKind().Kind
	self := localObj.(metav1.ObjectMetaAccessor).GetObjectMeta().GetSelfLink()

	glog.V(5).Infof("%s instance %s deleted from managed cluster", kind, self)
	//resv1.RemoveFinalizerAndPut(ctx, localObj, Name+common.Finalizer)
	common.RemoveFinalizerAndPut(ctx, localObj, myFinalizerNames)
	return nil
}

// create an object replica annotating it with traceback information
func createReplicatedObject(obj runtime.Object, client *seedclient.ResourceClient) error {
	metadata := obj.(metav1.ObjectMetaAccessor).GetObjectMeta()
	metadata.SetResourceVersion("")

	if common.HasOwnerReferencesOf(obj, "Policy") {
		// ownerReferences := []metav1.OwnerReference{
		// 	*metav1.NewControllerRef(obj.(metav1.Object), schema.GroupVersionKind{
		// 		Group:   policyv1alpha1.SchemeGroupVersion.Group,
		// 		Version: policyv1alpha1.SchemeGroupVersion.Version,
		// 		Kind:    "Policy",
		// 	}),
		// }
		metadata.SetOwnerReferences([]metav1.OwnerReference{})
	}
	// metadata.SetAnnotations(map[string]string{
	// 	"ownerUID": string(metadata.GetUID()),
	// 	// consider adding other annotations, such as replicated_at, replicated_version or generation, etc.
	// })
	glog.V(6).Infof("createReplicatedObject: %s", metadata)
	return client.Create(obj)
}

// update the named field on target object to match the source object's
func updateField(field string, source, target runtime.Object, client *seedclient.ResourceClient) (bool, error) {
	updated := false
	sourceValue := util.GetField(source, field)
	targetValue := util.GetField(target, field)

	name := source.(metav1.ObjectMetaAccessor).GetObjectMeta().GetName()
	if !equality.Semantic.DeepEqual(sourceValue, targetValue) { // specs have diverged
		glog.V(5).Infof("updating field %s on target object %s", field, name)
		updated = true

		util.SetField(target, field, sourceValue)
		err := client.Update(target)
		if err != nil {
			return false, fmt.Errorf("error updating field %s on target object: %s", field, err)
		}
	} else {
		glog.V(5).Infof("nothing to update field %s on target object %s", field, name)
	}
	return updated, nil
}
