package webhooks

import (
	"context"
	"encoding/json"
	"fmt"
	"net/http"

	"github.com/google/uuid"
	admissionv1 "k8s.io/api/admission/v1"
	corev1 "k8s.io/api/core/v1"
	"k8s.io/client-go/tools/record"
	ctrl "sigs.k8s.io/controller-runtime"
	"sigs.k8s.io/controller-runtime/pkg/client"
	logf "sigs.k8s.io/controller-runtime/pkg/log"
	"sigs.k8s.io/controller-runtime/pkg/webhook/admission"

	"github.com/tidbcloud/infra-provider/pkg/gitops-sdk/consts"

	cloudv1 "github.com/tidbcloud/infra-api/api/v1"
	sdk "github.com/tidbcloud/infra-provider/pkg/gitops-sdk"
	"github.com/tidbcloud/infra-provider/pkg/gitops-sdk/revision"
)

// Note: do not change the webhook name, we are controlling the order of different webhooks by webhook name (alphabetical order).

//+kubebuilder:webhook:path=/mutate-cloud-pingcap-com-v1-cluster-revision,mutating=true,failurePolicy=fail,sideEffects=None,groups=cloud.pingcap.com,resources=clusters,verbs=create;update,versions=v1,name=revision.kb.io,admissionReviewVersions={v1,v1beta1}

var (
	glogger                   = logf.Log.WithName("cluster-webhook")
	_       admission.Handler = &ClusterRevisionMutator{}
)

type ClusterRevisionMutator struct {
	cli           client.Client
	eventRecorder record.EventRecorder
	g             sdk.CCInterface
	rv            revision.Interface
	decoder       admission.Decoder
}

func NewClusterRevisionMutator(mgr ctrl.Manager) *ClusterRevisionMutator {
	cli := mgr.GetClient()
	return &ClusterRevisionMutator{
		cli:           cli,
		eventRecorder: mgr.GetEventRecorderFor("clusters-webhook"),
		g:             &sdk.Gitops{Cli: cli},
		rv:            revision.NewRevisioner(cli),
		decoder:       admission.NewDecoder(mgr.GetScheme()),
	}
}

func (c *ClusterRevisionMutator) Handle(ctx context.Context, req admission.Request) admission.Response {
	cluster := &cloudv1.Cluster{}
	err := c.decoder.Decode(req, cluster)
	if err != nil {
		return admission.Errored(http.StatusBadRequest, err)
	}
	logger := glogger.WithValues("cluster", cluster.Name, "logid", uuid.New().String())
	ctx = logf.IntoContext(ctx, logger)

	providerType := cluster.GetProviderType()
	switch providerType {
	case consts.ProviderTypeAWSFreeTier, consts.ProviderTypeAWSSharedTier, consts.ProviderTypeGCPSharedTier:
		logger.Info("skip for serverless cluster", "provider", providerType)
		return admission.ValidationResponse(true, "serverless cluster")
	case consts.ProviderTypeAWS, consts.ProviderTypeGCP:
		logger.Info("skip for gardener-arch cluster", "provider", providerType)
		return admission.ValidationResponse(true, "gardener-based cluster")
	}
	logger.Info("deal with new-arch cluster", "provider", providerType)

	switch req.Operation {
	case admissionv1.Create:
		return c.handleCreate(ctx, req)
	case admissionv1.Update:
		return c.handleUpdate(ctx, req)
	default:
		return admission.ValidationResponse(true, fmt.Sprintf("ignored operation %s", req.Operation))
	}
}

func (c *ClusterRevisionMutator) handleCreate(ctx context.Context, req admission.Request) admission.Response {
	logger := logf.FromContext(ctx)
	cluster := &cloudv1.Cluster{}
	err := c.decoder.Decode(req, cluster)
	if err != nil {
		return admission.Errored(http.StatusBadRequest, err)
	}

	if cluster.Spec.ConfigRevisionRef != nil {
		return admission.ValidationResponse(true, "ConfigRevisionRef already set")
	}

	origin, err := json.Marshal(cluster)
	if err != nil {
		return admission.Errored(http.StatusBadRequest, err)
	}

	if cluster.Spec.ConfigRevisionRef == nil {
		logger.Info("calculate configRevision for cluster", "name", req.Name)
		cc, err := c.g.DescribeClusterConfiguration(cluster)
		if err != nil {
			return admission.Errored(http.StatusInternalServerError, err)
		}
		// use clusterconfig for newly created cluster no matter if updatepolicy is dryrun.
		cr, err := sdk.GetClusterRevision(c.rv, cc, true)
		if err != nil {
			return admission.Errored(http.StatusInternalServerError, err)
		}
		// we should always set the version Guard when creating a cluster, to allow set cluster version to any we want.
		cluster.UpdateConfigRevision(cr, true)
	}

	marshaledCluster, err := json.Marshal(cluster)
	if err != nil {
		return admission.Errored(http.StatusInternalServerError, err)
	}
	return admission.PatchResponseFromRaw(origin, marshaledCluster)
}

func (c *ClusterRevisionMutator) handleUpdate(ctx context.Context, req admission.Request) admission.Response {
	logger := logf.FromContext(ctx)

	newCluster := &cloudv1.Cluster{}
	if err := c.decoder.Decode(req, newCluster); err != nil {
		return admission.Errored(http.StatusBadRequest, fmt.Errorf("decode new: %w", err))
	}
	oldCluster := &cloudv1.Cluster{}
	if err := c.decoder.DecodeRaw(req.OldObject, oldCluster); err != nil {
		return admission.Errored(http.StatusBadRequest, fmt.Errorf("decode old: %w", err))
	}

	var instanceChanged []cloudv1.ComponentType
	for _, comp := range []cloudv1.ComponentType{cloudv1.ComponentPD, cloudv1.ComponentTiProxy, cloudv1.ComponentTiDB, cloudv1.ComponentTiKV, cloudv1.ComponentTiFlash} {
		oldComp := oldCluster.GetComponent(comp)
		newComp := newCluster.GetComponent(comp)

		if oldComp.GetInstanceType() != newComp.GetInstanceType() {
			instanceChanged = append(instanceChanged, comp)
		}
	}
	if len(instanceChanged) == 0 {
		logger.Info("no instance changed, skip")
		return admission.ValidationResponse(true, "no instance changed")
	}
	logger.Info("instance type changed components in Cluster update", "changed", instanceChanged)

	rev, err := sdk.ReCalculateConfigRevision(c.cli, newCluster, instanceChanged)
	if err != nil {
		return admission.Errored(http.StatusInternalServerError, fmt.Errorf("recalculate ConfigRevision: %w", err))
	}
	oldCRName := newCluster.Spec.ConfigRevisionRef.Name
	logger.Info("generate new ConfigRevision", "new", rev.Name, "old", oldCRName)
	c.eventRecorder.Eventf(newCluster, corev1.EventTypeNormal, "ConfigRevisionCreated", "cased by instance changed, old=%s, new=%s, instanceChagned=%v", oldCRName, rev.Name, instanceChanged)

	origin, err := json.Marshal(newCluster)
	if err != nil {
		return admission.Errored(http.StatusBadRequest, err)
	}
	// In here, we expect only the config changes to be applied, and version change is NOT allowed.
	newCluster.UpdateConfigRevision(rev, false)
	modified, err := json.Marshal(newCluster)
	if err != nil {
		return admission.Errored(http.StatusInternalServerError, err)
	}

	c.eventRecorder.Eventf(newCluster, corev1.EventTypeNormal, "ConfigRevisionChanged", "modified in webhook, old=%s, new=%s", oldCRName, rev.Name)
	return admission.PatchResponseFromRaw(origin, modified)
}
