package virtualgateway

import (
	"context"

	appmesh "github.com/aws/aws-app-mesh-controller-for-k8s/apis/appmesh/v1beta2"
	"github.com/aws/aws-app-mesh-controller-for-k8s/pkg/aws/services"
	"github.com/aws/aws-app-mesh-controller-for-k8s/pkg/conversions"
	"github.com/aws/aws-app-mesh-controller-for-k8s/pkg/equality"
	"github.com/aws/aws-app-mesh-controller-for-k8s/pkg/k8s"
	"github.com/aws/aws-app-mesh-controller-for-k8s/pkg/mesh"
	"github.com/aws/aws-app-mesh-controller-for-k8s/pkg/references"
	"github.com/aws/aws-app-mesh-controller-for-k8s/pkg/runtime"
	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/aws/awserr"
	appmeshsdk "github.com/aws/aws-sdk-go/service/appmesh"
	"github.com/go-logr/logr"
	"github.com/google/go-cmp/cmp"
	"github.com/pkg/errors"
	corev1 "k8s.io/api/core/v1"
	"k8s.io/apimachinery/pkg/conversion"
	"sigs.k8s.io/controller-runtime/pkg/client"
)

// ResourceManager is dedicated to manage AppMesh VirtualGateway resources for k8s VirtualGateway CRs.
type ResourceManager interface {
	// Reconcile will create/update AppMesh VirtualGateway to match vg.spec, and update vg.status
	Reconcile(ctx context.Context, vg *appmesh.VirtualGateway) error

	// Cleanup will delete AppMesh VirtualGateway created for vg.
	Cleanup(ctx context.Context, vg *appmesh.VirtualGateway) error
}

func NewDefaultResourceManager(
	k8sClient client.Client,
	appMeshSDK services.AppMesh,
	referencesResolver references.Resolver,
	accountID string,
	log logr.Logger) ResourceManager {

	return &defaultResourceManager{
		k8sClient:          k8sClient,
		appMeshSDK:         appMeshSDK,
		referencesResolver: referencesResolver,
		accountID:          accountID,
		log:                log,
	}
}

// defaultResourceManager implements ResourceManager
type defaultResourceManager struct {
	k8sClient          client.Client
	appMeshSDK         services.AppMesh
	referencesResolver references.Resolver
	accountID          string
	log                logr.Logger
}

func (m *defaultResourceManager) Reconcile(ctx context.Context, vg *appmesh.VirtualGateway) error {
	ms, err := m.findMeshDependency(ctx, vg)
	if err != nil {
		return err
	}
	if err := m.validateMeshDependencies(ctx, ms); err != nil {
		return err
	}

	sdkVG, err := m.findSDKVirtualGateway(ctx, ms, vg)
	if err != nil {
		return err
	}
	if sdkVG == nil {
		sdkVG, err = m.createSDKVirtualGateway(ctx, ms, vg)
		if err != nil {
			return err
		}
	} else {
		sdkVG, err = m.updateSDKVirtualGateway(ctx, sdkVG, ms, vg)
		if err != nil {
			return err
		}
	}

	return m.updateCRDVirtualGateway(ctx, vg, sdkVG)
}

func (m *defaultResourceManager) Cleanup(ctx context.Context, vg *appmesh.VirtualGateway) error {
	ms, err := m.findMeshDependency(ctx, vg)
	if err != nil {
		return err
	}
	sdkVG, err := m.findSDKVirtualGateway(ctx, ms, vg)
	if err != nil {
		if vg.Status.VirtualGatewayARN == nil {
			return nil
		}
		return err
	}
	if sdkVG == nil {
		return nil
	}

	return m.deleteSDKVirtualGateway(ctx, sdkVG, ms, vg)
}

// findMeshDependency find the Mesh dependency for this virtualGateway.
func (m *defaultResourceManager) findMeshDependency(ctx context.Context, vg *appmesh.VirtualGateway) (*appmesh.Mesh, error) {
	if vg.Spec.MeshRef == nil {
		return nil, errors.Errorf("meshRef shouldn't be nil, please check webhook setup")
	}
	ms, err := m.referencesResolver.ResolveMeshReference(ctx, *vg.Spec.MeshRef)
	if err != nil {
		return nil, errors.Wrapf(err, "failed to resolve meshRef")
	}
	return ms, nil
}

// validateMeshDependencies validate the Mesh dependency for this virtualGateway.
func (m *defaultResourceManager) validateMeshDependencies(ctx context.Context, ms *appmesh.Mesh) error {
	if !mesh.IsMeshActive(ms) {
		return runtime.NewRequeueError(errors.New("mesh is not active yet"))
	}
	return nil
}

func (m *defaultResourceManager) findSDKVirtualGateway(ctx context.Context, ms *appmesh.Mesh, vg *appmesh.VirtualGateway) (*appmeshsdk.VirtualGatewayData, error) {
	resp, err := m.appMeshSDK.DescribeVirtualGatewayWithContext(ctx, &appmeshsdk.DescribeVirtualGatewayInput{
		MeshName:           ms.Spec.AWSName,
		MeshOwner:          ms.Spec.MeshOwner,
		VirtualGatewayName: vg.Spec.AWSName,
	})
	if err != nil {
		if awsErr, ok := err.(awserr.Error); ok && awsErr.Code() == "NotFoundException" {
			return nil, nil
		}
		return nil, err
	}

	return resp.VirtualGateway, nil
}

func (m *defaultResourceManager) createSDKVirtualGateway(ctx context.Context, ms *appmesh.Mesh, vg *appmesh.VirtualGateway) (*appmeshsdk.VirtualGatewayData, error) {
	sdkVGSpec, err := BuildSDKVirtualGatewaySpec(ctx, vg)
	if err != nil {
		return nil, err
	}
	resp, err := m.appMeshSDK.CreateVirtualGatewayWithContext(ctx, &appmeshsdk.CreateVirtualGatewayInput{
		MeshName:           ms.Spec.AWSName,
		MeshOwner:          ms.Spec.MeshOwner,
		Spec:               sdkVGSpec,
		VirtualGatewayName: vg.Spec.AWSName,
	})
	if err != nil {
		return nil, err
	}
	return resp.VirtualGateway, nil
}

func (m *defaultResourceManager) updateSDKVirtualGateway(ctx context.Context, sdkVG *appmeshsdk.VirtualGatewayData, ms *appmesh.Mesh, vg *appmesh.VirtualGateway) (*appmeshsdk.VirtualGatewayData, error) {
	actualSDKVGSpec := sdkVG.Spec
	desiredSDKVGSpec, err := BuildSDKVirtualGatewaySpec(ctx, vg)
	if err != nil {
		return nil, err
	}

	opts := equality.CompareOptionForVirtualGatewaySpec()
	if cmp.Equal(desiredSDKVGSpec, actualSDKVGSpec, opts) {
		return sdkVG, nil
	}
	if !m.isSDKVirtualGatewayControlledByCRDVirtualGateway(ctx, sdkVG, vg) {
		m.log.V(2).Info("skip virtualGateway update since it's not controlled",
			"virtualGateway", k8s.NamespacedName(vg),
			"virtualGatewayARN", aws.StringValue(sdkVG.Metadata.Arn),
		)
		return sdkVG, nil
	}

	diff := cmp.Diff(desiredSDKVGSpec, actualSDKVGSpec, opts)
	m.log.V(2).Info("virtualGatewaySpec changed",
		"virtualGateway", k8s.NamespacedName(vg),
		"actualSDKVGSpec", actualSDKVGSpec,
		"desiredSDKVGSpec", desiredSDKVGSpec,
		"diff", diff,
	)
	resp, err := m.appMeshSDK.UpdateVirtualGatewayWithContext(ctx, &appmeshsdk.UpdateVirtualGatewayInput{
		MeshName:           ms.Spec.AWSName,
		MeshOwner:          ms.Spec.MeshOwner,
		Spec:               desiredSDKVGSpec,
		VirtualGatewayName: sdkVG.VirtualGatewayName,
	})
	if err != nil {
		return nil, err
	}
	return resp.VirtualGateway, nil
}

func (m *defaultResourceManager) deleteSDKVirtualGateway(ctx context.Context, sdkVG *appmeshsdk.VirtualGatewayData, ms *appmesh.Mesh, vg *appmesh.VirtualGateway) error {
	if !m.isSDKVirtualGatewayOwnedByCRDVirtualGateway(ctx, sdkVG, vg) {
		m.log.V(2).Info("skip mesh virtualGateway since its not owned",
			"virtualGateway", k8s.NamespacedName(vg),
			"virtualGatewayARN", aws.StringValue(sdkVG.Metadata.Arn),
		)
		return nil
	}

	_, err := m.appMeshSDK.DeleteVirtualGatewayWithContext(ctx, &appmeshsdk.DeleteVirtualGatewayInput{
		MeshName:           ms.Spec.AWSName,
		MeshOwner:          ms.Spec.MeshOwner,
		VirtualGatewayName: sdkVG.VirtualGatewayName,
	})
	if err != nil {
		return err
	}
	return nil
}

func (m *defaultResourceManager) updateCRDVirtualGateway(ctx context.Context, vg *appmesh.VirtualGateway, sdkVG *appmeshsdk.VirtualGatewayData) error {
	oldVG := vg.DeepCopy()
	needsUpdate := false
	if aws.StringValue(vg.Status.VirtualGatewayARN) != aws.StringValue(sdkVG.Metadata.Arn) {
		vg.Status.VirtualGatewayARN = sdkVG.Metadata.Arn
		needsUpdate = true
	}

	vgActiveConditionStatus := corev1.ConditionFalse
	if sdkVG.Status != nil && aws.StringValue(sdkVG.Status.Status) == appmeshsdk.VirtualGatewayStatusCodeActive {
		vgActiveConditionStatus = corev1.ConditionTrue
	}
	if updateCondition(vg, appmesh.VirtualGatewayActive, vgActiveConditionStatus, nil, nil) {
		needsUpdate = true
	}

	if !needsUpdate {
		return nil
	}
	return m.k8sClient.Status().Patch(ctx, vg, client.MergeFrom(oldVG))
}

func (m *defaultResourceManager) buildSDKVirtualGatewayTags(ctx context.Context, vg *appmesh.VirtualGateway) []*appmeshsdk.TagRef {
	// TODO, support tags
	return nil
}

// isSDKVirtualGatewayControlledByCRDVirtualGateway checks whether an AppMesh virtualGateway is controlled by CRD virtualGateway
// if it's controlled, CRD virtualGateway update is responsible for update AppMesh virtualGateway.
func (m *defaultResourceManager) isSDKVirtualGatewayControlledByCRDVirtualGateway(ctx context.Context, sdkVG *appmeshsdk.VirtualGatewayData, vg *appmesh.VirtualGateway) bool {
	if aws.StringValue(sdkVG.Metadata.ResourceOwner) != m.accountID {
		return false
	}
	return true
}

// isSDKVirtualGatewayOwnedByCRDVirtualGateway checks whether an AppMesh virtualGateway is owned by CRD virtualGateway.
// if it's owned, CRD virtualGateway deletion is responsible for delete AppMesh virtualGateway.
func (m *defaultResourceManager) isSDKVirtualGatewayOwnedByCRDVirtualGateway(ctx context.Context, sdkVG *appmeshsdk.VirtualGatewayData, vg *appmesh.VirtualGateway) bool {
	if !m.isSDKVirtualGatewayControlledByCRDVirtualGateway(ctx, sdkVG, vg) {
		return false
	}

	// TODO: Add tagging support.
	// currently, virtualGateway control == ownership, but it doesn't have to be so once we add tagging support.
	return true
}

func BuildSDKVirtualGatewaySpec(ctx context.Context, vg *appmesh.VirtualGateway) (*appmeshsdk.VirtualGatewaySpec, error) {
	converter := conversion.NewConverter(conversion.DefaultNameFunc)
	converter.RegisterUntypedConversionFunc((*appmesh.VirtualGatewaySpec)(nil), (*appmeshsdk.VirtualGatewaySpec)(nil), func(a, b interface{}, scope conversion.Scope) error {
		return conversions.Convert_CRD_VirtualGatewaySpec_To_SDK_VirtualGatewaySpec(a.(*appmesh.VirtualGatewaySpec), b.(*appmeshsdk.VirtualGatewaySpec), scope)
	})

	sdkVGSpec := &appmeshsdk.VirtualGatewaySpec{}
	if err := converter.Convert(&vg.Spec, sdkVGSpec, nil); err != nil {
		return nil, err
	}
	return sdkVGSpec, nil
}
