package router

import (
	"encoding/json"
	"fmt"

	"github.com/google/go-cmp/cmp"
	"github.com/google/go-cmp/cmp/cmpopts"
	"go.uber.org/zap"
	"k8s.io/apimachinery/pkg/api/errors"
	"k8s.io/apimachinery/pkg/api/resource"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime/schema"
	"k8s.io/client-go/kubernetes"

	flaggerv1 "github.com/weaveworks/flagger/pkg/apis/flagger/v1beta1"
	smiv1alpha1 "github.com/weaveworks/flagger/pkg/apis/smi/v1alpha1"
	smiv1alpha2 "github.com/weaveworks/flagger/pkg/apis/smi/v1alpha2"
	clientset "github.com/weaveworks/flagger/pkg/client/clientset/versioned"
)

type SmiRouter struct {
	kubeClient    kubernetes.Interface
	flaggerClient clientset.Interface
	smiClient     clientset.Interface
	logger        *zap.SugaredLogger
	targetMesh    string
}

// Reconcile creates or updates the SMI traffic split
func (sr *SmiRouter) Reconcile(canary *flaggerv1.Canary) error {
	apexName, primaryName, canaryName := canary.GetServiceNames()

	var host string
	if len(canary.Spec.Service.Hosts) > 0 {
		host = canary.Spec.Service.Hosts[0]
	} else {
		host = apexName
	}

	tsSpec := smiv1alpha1.TrafficSplitSpec{
		Service: host,
		Backends: []smiv1alpha1.TrafficSplitBackend{
			{
				Service: canaryName,
				Weight:  resource.NewQuantity(0, resource.DecimalExponent),
			},
			{
				Service: primaryName,
				Weight:  resource.NewQuantity(100, resource.DecimalExponent),
			},
		},
	}

	ts, err := sr.smiClient.SplitV1alpha1().TrafficSplits(canary.Namespace).Get(apexName, metav1.GetOptions{})
	// create traffic split
	if errors.IsNotFound(err) {
		t := &smiv1alpha1.TrafficSplit{
			ObjectMeta: metav1.ObjectMeta{
				Name:      apexName,
				Namespace: canary.Namespace,
				OwnerReferences: []metav1.OwnerReference{
					*metav1.NewControllerRef(canary, schema.GroupVersionKind{
						Group:   flaggerv1.SchemeGroupVersion.Group,
						Version: flaggerv1.SchemeGroupVersion.Version,
						Kind:    flaggerv1.CanaryKind,
					}),
				},
				Annotations: sr.makeAnnotations(canary.Spec.Service.Gateways),
			},
			Spec: tsSpec,
		}

		_, err := sr.smiClient.SplitV1alpha1().TrafficSplits(canary.Namespace).Create(t)
		if err != nil {
			return fmt.Errorf("TrafficSplit %s.%s create error: %w", apexName, canary.Namespace, err)
		}

		sr.logger.With("canary", fmt.Sprintf("%s.%s", canary.Name, canary.Namespace)).
			Infof("TrafficSplit %s.%s created", t.GetName(), canary.Namespace)
		return nil
	} else if err != nil {
		return fmt.Errorf("TrafficSplit %s.%s get query error: %w", apexName, canary.Namespace, err)
	}

	// update traffic split
	if diff := cmp.Diff(tsSpec, ts.Spec, cmpopts.IgnoreTypes(resource.Quantity{})); diff != "" {
		tsClone := ts.DeepCopy()
		tsClone.Spec = tsSpec

		_, err := sr.smiClient.SplitV1alpha1().TrafficSplits(canary.Namespace).Update(tsClone)
		if err != nil {
			return fmt.Errorf("TrafficSplit %s.%s update error: %w", apexName, canary.Namespace, err)
		}

		sr.logger.With("canary", fmt.Sprintf("%s.%s", canary.Name, canary.Namespace)).
			Infof("TrafficSplit %s.%s updated", apexName, canary.Namespace)
		return nil
	}

	return nil
}

// GetRoutes returns the destinations weight for primary and canary
func (sr *SmiRouter) GetRoutes(canary *flaggerv1.Canary) (
	primaryWeight int,
	canaryWeight int,
	mirrored bool,
	err error,
) {
	apexName, primaryName, canaryName := canary.GetServiceNames()
	ts, err := sr.smiClient.SplitV1alpha1().TrafficSplits(canary.Namespace).Get(apexName, metav1.GetOptions{})
	if err != nil {
		err = fmt.Errorf("TrafficSplit %s.%s get query error %v", apexName, canary.Namespace, err)
		return
	}

	for _, r := range ts.Spec.Backends {
		w, _ := r.Weight.AsInt64()
		if r.Service == primaryName {
			primaryWeight = int(w)
		}
		if r.Service == canaryName {
			canaryWeight = int(w)
		}
	}

	if primaryWeight == 0 && canaryWeight == 0 {
		err = fmt.Errorf("TrafficSplit %s.%s does not contain routes for %s and %s",
			apexName, canary.Namespace, primaryName, canaryName)
	}

	mirrored = false

	return
}

// SetRoutes updates the destinations weight for primary and canary
func (sr *SmiRouter) SetRoutes(
	canary *flaggerv1.Canary,
	primaryWeight int,
	canaryWeight int,
	_ bool,
) error {
	apexName, primaryName, canaryName := canary.GetServiceNames()
	ts, err := sr.smiClient.SplitV1alpha1().TrafficSplits(canary.Namespace).Get(apexName, metav1.GetOptions{})
	if err != nil {
		return fmt.Errorf("TrafficSplit %s.%s get query error %v", apexName, canary.Namespace, err)
	}

	backends := []smiv1alpha1.TrafficSplitBackend{
		{
			Service: canaryName,
			Weight:  resource.NewQuantity(int64(canaryWeight), resource.DecimalExponent),
		},
		{
			Service: primaryName,
			Weight:  resource.NewQuantity(int64(primaryWeight), resource.DecimalExponent),
		},
	}

	tsClone := ts.DeepCopy()
	tsClone.Spec.Backends = backends

	_, err = sr.smiClient.SplitV1alpha1().TrafficSplits(canary.Namespace).Update(tsClone)
	if err != nil {
		return fmt.Errorf("TrafficSplit %s.%s update error %v", apexName, canary.Namespace, err)
	}

	return nil
}

func (sr *SmiRouter) makeAnnotations(gateways []string) map[string]string {
	res := make(map[string]string)
	if sr.targetMesh == "istio" && len(gateways) > 0 {
		g, _ := json.Marshal(gateways)
		res["VirtualService.v1alpha3.networking.istio.io/spec.gateways"] = string(g)
	}
	return res
}

// getWithConvert overrides invalid traffic split and sets weight based on the canary status
func (sr *SmiRouter) getWithConvert(canary *flaggerv1.Canary, host string) (*smiv1alpha2.TrafficSplit, error) {
	apexName, primaryName, canaryName := canary.GetServiceNames()
	ts, err := sr.smiClient.SplitV1alpha2().TrafficSplits(canary.Namespace).Get(apexName, metav1.GetOptions{})
	if errors.IsInvalid(err) {
		t := &smiv1alpha2.TrafficSplit{
			ObjectMeta: metav1.ObjectMeta{
				Name:      apexName,
				Namespace: canary.Namespace,
				OwnerReferences: []metav1.OwnerReference{
					*metav1.NewControllerRef(canary, schema.GroupVersionKind{
						Group:   flaggerv1.SchemeGroupVersion.Group,
						Version: flaggerv1.SchemeGroupVersion.Version,
						Kind:    flaggerv1.CanaryKind,
					}),
				},
				Annotations: sr.makeAnnotations(canary.Spec.Service.Gateways),
			},
			Spec: smiv1alpha2.TrafficSplitSpec{
				Service: host,
				Backends: []smiv1alpha2.TrafficSplitBackend{
					{
						Service: canaryName,
						Weight:  canary.Status.CanaryWeight,
					},
					{
						Service: primaryName,
						Weight:  100 - canary.Status.CanaryWeight,
					},
				},
			},
		}

		_, err := sr.smiClient.SplitV1alpha2().TrafficSplits(canary.Namespace).Update(t)
		if err != nil {
			return nil, fmt.Errorf("TrafficSplit %s.%s update error: %w", apexName, canary.Namespace, err)
		}

		sr.logger.With("canary", fmt.Sprintf("%s.%s", canary.Name, canary.Namespace)).
			Infof("TrafficSplit %s.%s converted", t.GetName(), canary.Namespace)
	} else if err != nil {
		return nil, fmt.Errorf("TrafficSplit %s.%s get query error %v", apexName, canary.Namespace, err)
	}
	return ts, nil
}
