package clusterapi

import (
	"context"
	"reflect"

	"github.com/pkg/errors"
	"k8s.io/apimachinery/pkg/api/equality"
	apierrors "k8s.io/apimachinery/pkg/api/errors"
	clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
	kubeadmv1 "sigs.k8s.io/cluster-api/bootstrap/kubeadm/api/v1beta1"

	anywherev1 "github.com/aws/eks-anywhere/pkg/api/v1alpha1"
	"github.com/aws/eks-anywhere/pkg/clients/kubernetes"
)

// Workers represents the provider specific CAPI spec for an eks-a cluster's workers.
type Workers[M Object[M]] struct {
	Groups []WorkerGroup[M]
}

// WorkerObjects returns a list of API objects for concrete provider-specific collection of worker groups.
func (w *Workers[M]) WorkerObjects() []kubernetes.Object {
	objs := make([]kubernetes.Object, 0, len(w.Groups)*3)
	for _, g := range w.Groups {
		objs = append(objs, g.Objects()...)
	}

	return objs
}

// UpdateImmutableObjectNames checks if any immutable objects have changed by comparing the new definition
// with the current state of the cluster. If they had, it generates a new name for them by increasing a monotonic number
// at the end of the name.
func (w *Workers[M]) UpdateImmutableObjectNames(
	ctx context.Context,
	client kubernetes.Client,
	machineTemplateRetriever ObjectRetriever[M],
	machineTemplateComparator ObjectComparator[M],
) error {
	for _, g := range w.Groups {
		if err := g.UpdateImmutableObjectNames(ctx, client, machineTemplateRetriever, machineTemplateComparator); err != nil {
			return err
		}
	}

	return nil
}

// WorkerGroup represents the provider specific CAPI spec for an eks-a worker group.
type WorkerGroup[M Object[M]] struct {
	KubeadmConfigTemplate   *kubeadmv1.KubeadmConfigTemplate
	MachineDeployment       *clusterv1.MachineDeployment
	ProviderMachineTemplate M
}

// Objects returns a list of API objects for a provider-specific of the worker group.
func (g *WorkerGroup[M]) Objects() []kubernetes.Object {
	return []kubernetes.Object{
		g.KubeadmConfigTemplate,
		g.MachineDeployment,
		g.ProviderMachineTemplate,
	}
}

// UpdateImmutableObjectNames checks if any immutable objects have changed by comparing the new definition
// with the current state of the cluster. If they had, it generates a new name for them by increasing a monotonic number
// at the end of the name.
// This process is performed to the provider machine template and the kubeadmconfigtemplate.
// The kubeadmconfigtemplate is not immutable at the API level but we treat it as such for consistency.
func (g *WorkerGroup[M]) UpdateImmutableObjectNames(
	ctx context.Context,
	client kubernetes.Client,
	machineTemplateRetriever ObjectRetriever[M],
	machineTemplateComparator ObjectComparator[M],
) error {
	currentMachineDeployment := &clusterv1.MachineDeployment{}
	err := client.Get(ctx, g.MachineDeployment.Name, g.MachineDeployment.Namespace, currentMachineDeployment)
	if apierrors.IsNotFound(err) {
		// MachineDeployment doesn't exist, this is a new cluster so machine templates should use their default name
		return nil
	}
	if err != nil {
		return errors.Wrap(err, "reading current machine deployment from API")
	}

	g.ProviderMachineTemplate.SetName(currentMachineDeployment.Spec.Template.Spec.InfrastructureRef.Name)
	if err = EnsureNewNameIfChanged(ctx, client, machineTemplateRetriever, machineTemplateComparator, g.ProviderMachineTemplate); err != nil {
		return err
	}
	g.MachineDeployment.Spec.Template.Spec.InfrastructureRef.Name = g.ProviderMachineTemplate.GetName()

	g.KubeadmConfigTemplate.SetName(currentMachineDeployment.Spec.Template.Spec.Bootstrap.ConfigRef.Name)
	if err = EnsureNewNameIfChanged(ctx, client, GetKubeadmConfigTemplate, KubeadmConfigTemplateEqual, g.KubeadmConfigTemplate); err != nil {
		return err
	}
	g.MachineDeployment.Spec.Template.Spec.Bootstrap.ConfigRef.Name = g.KubeadmConfigTemplate.Name

	return nil
}

// DeepCopy generates a new WorkerGroup copying the contexts of the receiver.
func (g *WorkerGroup[M]) DeepCopy() *WorkerGroup[M] {
	return &WorkerGroup[M]{
		MachineDeployment:       g.MachineDeployment.DeepCopy(),
		KubeadmConfigTemplate:   g.KubeadmConfigTemplate.DeepCopy(),
		ProviderMachineTemplate: g.ProviderMachineTemplate.DeepCopy(),
	}
}

// GetKubeadmConfigTemplate retrieves a KubeadmConfigTemplate using a client
// Implements ObjectRetriever.
func GetKubeadmConfigTemplate(ctx context.Context, client kubernetes.Client, name, namespace string) (*kubeadmv1.KubeadmConfigTemplate, error) {
	k := &kubeadmv1.KubeadmConfigTemplate{}
	if err := client.Get(ctx, name, namespace, k); err != nil {
		return nil, err
	}

	return k, nil
}

// KubeadmConfigTemplateEqual returns true only if the new version of a KubeadmConfigTemplate
// involves changes with respect to the old one when applied to the cluster.
// Implements ObjectComparator.
func KubeadmConfigTemplateEqual(new, old *kubeadmv1.KubeadmConfigTemplate) bool {
	// DeepDerivative treats empty map (length == 0) as unset field. We need to manually compare certain fields
	// such as taints, so that setting it to empty will trigger machine recreate
	// The file check with deep equal has been added since the introduction of kubelet configuration in case users
	// want to get rid of the files with that context.
	return kubeadmConfigTemplateTaintsEqual(new, old) && kubeadmConfigTemplateExtraArgsEqual(new, old) &&
		reflect.DeepEqual(new.Spec.Template.Spec.Files, old.Spec.Template.Spec.Files) &&
		equality.Semantic.DeepDerivative(new.Spec, old.Spec)
}

func kubeadmConfigTemplateTaintsEqual(new, old *kubeadmv1.KubeadmConfigTemplate) bool {
	return new.Spec.Template.Spec.JoinConfiguration == nil ||
		old.Spec.Template.Spec.JoinConfiguration == nil ||
		anywherev1.TaintsSliceEqual(
			new.Spec.Template.Spec.JoinConfiguration.NodeRegistration.Taints,
			old.Spec.Template.Spec.JoinConfiguration.NodeRegistration.Taints,
		)
}

func kubeadmConfigTemplateExtraArgsEqual(new, old *kubeadmv1.KubeadmConfigTemplate) bool {
	return new.Spec.Template.Spec.JoinConfiguration == nil ||
		old.Spec.Template.Spec.JoinConfiguration == nil ||
		anywherev1.MapEqual(
			new.Spec.Template.Spec.JoinConfiguration.NodeRegistration.KubeletExtraArgs,
			old.Spec.Template.Spec.JoinConfiguration.NodeRegistration.KubeletExtraArgs,
		)
}
