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

package sync

import (
	"errors"

	"k8s.io/client-go/rest"
	"k8s.io/client-go/tools/record"

	seedclient "github.com/open-cluster-management/seed-sdk/pkg/client"
	"github.com/open-cluster-management/seed-sdk/pkg/context"
	seedcontroller "github.com/open-cluster-management/seed-sdk/pkg/controller"
	"github.com/open-cluster-management/seed-sdk/pkg/scheme"

	"github.com/open-cluster-management/hcm-compliance/pkg/apis/policy/v1alpha1"
	"github.com/open-cluster-management/hcm-compliance/pkg/common"
)

// NewPolicyClient returns a new typed resource client for Policy instances
func NewPolicyClient(cfg *rest.Config) (*seedclient.ResourceClient, error) {
	if cfg == nil {
		return nil, errors.New("nil config for NewPolicyClient()")
	}

	ks := scheme.NewScheme()
	v1alpha1.AddToScheme(ks.Scheme)
	ks.SetPlural(v1alpha1.SchemeGroupVersion.WithKind("Policy"), v1alpha1.PolicyResourcePlural)

	return seedclient.New(cfg, v1alpha1.SchemeGroupVersion, ks)
}

// NewPolicySyncher creates a new Policy synchronization handler
func NewPolicySyncher(hubClusterConfig, managedClusterConfig *rest.Config) (*PolicySyncher, error) {
	cs := &PolicySyncher{}
	var err error

	if cs.hub, err = NewPolicyClient(hubClusterConfig); err != nil {
		return nil, err
	}
	if cs.managed, err = NewPolicyClient(managedClusterConfig); err != nil {
		return nil, err
	}
	cs.inplace = sameAPIEndpopint(hubClusterConfig, managedClusterConfig)

	if cs.hubRecorder, err = common.CreateRecorder(cs.hub.KubeClient, "sync-controller"); err != nil {
		return nil, err
	}

	if cs.managedRecorder, err = common.CreateRecorder(cs.managed.KubeClient, "sync-controller"); err != nil {
		return nil, err
	}
	// @todo consider source and destination namespace names as well (once supported)
	return cs, nil
}

// WatchSpecEvents watches for Policy.Spec events and installs handlers for them
func (aps *PolicySyncher) WatchSpecEvents(cb seedcontroller.Builder) seedcontroller.WatchBuilder {
	return cb.Watch("policies.v1alpha1.policy.mcm.ibm.com", "Policy", &v1alpha1.SchemeBuilder).
		Reconcile(aps.hubReconcile).
		Finalize(aps.hubFinalize)
}

// WatchStatusEvents watches for Policy.Status events and installs handlers for them
func (aps *PolicySyncher) WatchStatusEvents(cb seedcontroller.Builder) seedcontroller.WatchBuilder {
	return cb.Watch("policies.v1alpha1.policy.mcm.ibm.com", "Policy", &v1alpha1.SchemeBuilder).
		Reconcile(aps.managedReconcile).
		Finalize(aps.managedFinalize)
}

// PolicySyncher encapsulates objects needed for synchronizing Policy instances
type PolicySyncher struct {
	inplace         bool
	hub             *seedclient.ResourceClient
	managed         *seedclient.ResourceClient
	hubRecorder     record.EventRecorder
	managedRecorder record.EventRecorder
}

// @todo the below functions can be removed by refactoring them as function closures in Watch* methods

// propagate specification changes from remote to local.
func (aps *PolicySyncher) hubReconcile(ctx context.Context, rObj *v1alpha1.Policy) error {
	if aps.inplace {
		return nil
	}
	return specReconcileEvent(ctx, rObj, aps.hub, aps.managed, aps.managedRecorder)
}

// handle propagation of deletion from remote to local object.
func (aps *PolicySyncher) hubFinalize(ctx context.Context, rObj *v1alpha1.Policy) error {
	if aps.inplace {
		return nil
	}
	return specFinalize(ctx, rObj, aps.hub, aps.managed)
}

// propagate status changes on local to remote.
func (aps *PolicySyncher) managedReconcile(ctx context.Context, lObj *v1alpha1.Policy) error {
	if aps.inplace {
		return nil
	}
	if common.HasOwnerReferencesOf(lObj, "Compliance") {
		return nil
	}
	return statusReconcileEvent(ctx, lObj, aps.hub, aps.managed, aps.hubRecorder)
}

// handle deletion of local (managed) instances
func (aps *PolicySyncher) managedFinalize(ctx context.Context, lObj *v1alpha1.Policy) error {
	if aps.inplace {
		return nil
	}
	return statusFinalize(ctx, lObj, aps.hub, aps.managed)
}
