/*
Copyright 2021 The Karmada Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package prune

import (
	"fmt"
	"strings"

	batchv1 "k8s.io/api/batch/v1"
	corev1 "k8s.io/api/core/v1"
	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
	storagevolume "k8s.io/component-helpers/storage/volume"
	utildeployment "k8s.io/kubectl/pkg/util/deployment"

	"github.com/karmada-io/karmada/pkg/util"
	"github.com/karmada-io/karmada/pkg/util/helper"
)

// pruneIrrelevantField is the function that prune irrelevant fields from Work Object.
type irrelevantFieldPruneFunc func(*unstructured.Unstructured) error

var kindIrrelevantFieldPruners = map[string]irrelevantFieldPruneFunc{
	util.DeploymentKind:            removeDeploymentIrrelevantField,
	util.JobKind:                   removeJobIrrelevantField,
	util.SecretKind:                removeSecretIrrelevantField,
	util.ServiceAccountKind:        removeServiceAccountIrrelevantField,
	util.ServiceKind:               removeServiceIrrelevantField,
	util.PersistentVolumeClaimKind: removePersistentVolumeClaimIrrelevantField,
}

// RemoveIrrelevantFields used to remove fields that generated by kube-apiserver and no need(or can't) propagate to
// member clusters.
func RemoveIrrelevantFields(workload *unstructured.Unstructured, extraHooks ...func(*unstructured.Unstructured)) error {
	// populated by the kubernetes.
	unstructured.RemoveNestedField(workload.Object, "metadata", "creationTimestamp")

	// populated by the kubernetes.
	// The kubernetes will set this fields in case of graceful deletion. This field is read-only and can't propagate to
	// member clusters.
	unstructured.RemoveNestedField(workload.Object, "metadata", "deletionTimestamp")

	// populated by the kubernetes.
	// The kubernetes will set this fields in case of graceful deletion. This field is read-only and can't propagate to
	// member clusters.
	unstructured.RemoveNestedField(workload.Object, "metadata", "deletionGracePeriodSeconds")

	// populated by the kubernetes.
	unstructured.RemoveNestedField(workload.Object, "metadata", "generation")

	// This is mostly for internal housekeeping, and users typically shouldn't need to set or understand this field.
	// Remove this field to keep 'Work' clean and tidy.
	unstructured.RemoveNestedField(workload.Object, "metadata", "managedFields")

	// populated by the kubernetes.
	unstructured.RemoveNestedField(workload.Object, "metadata", "resourceVersion")

	// populated by the kubernetes and has been deprecated by kubernetes.
	unstructured.RemoveNestedField(workload.Object, "metadata", "selfLink")

	// populated by the kubernetes.
	unstructured.RemoveNestedField(workload.Object, "metadata", "uid")

	unstructured.RemoveNestedField(workload.Object, "metadata", "ownerReferences")

	unstructured.RemoveNestedField(workload.Object, "metadata", "finalizers")

	unstructured.RemoveNestedField(workload.Object, "status")

	if pruneFunc, ok := kindIrrelevantFieldPruners[workload.GetKind()]; ok {
		if err := pruneFunc(workload); err != nil {
			return err
		}
	}

	for i := range extraHooks {
		extraHooks[i](workload)
	}
	return nil
}

func removeGenerateSelectorOfJob(workload *unstructured.Unstructured) error {
	matchLabels, exist, err := unstructured.NestedStringMap(workload.Object, "spec", "selector", "matchLabels")
	if err != nil {
		return err
	}
	if exist {
		delete(matchLabels, "controller-uid")
		// The label 'batch.kubernetes.io/controller-uid' was introduced at Kubernetes v1.27, which intend to replace
		// the previous label "controller-uid"(without batch.kubernetes.io prefix).
		// See https://github.com/kubernetes/kubernetes/pull/114930 for more details.
		delete(matchLabels, batchv1.ControllerUidLabel)

		err = unstructured.SetNestedStringMap(workload.Object, matchLabels, "spec", "selector", "matchLabels")
		if err != nil {
			return err
		}
	}

	templateLabels, exist, err := unstructured.NestedStringMap(workload.Object, "spec", "template", "metadata", "labels")
	if err != nil {
		return err
	}
	if exist {
		delete(templateLabels, "controller-uid")
		delete(templateLabels, "job-name")
		// The label 'batch.kubernetes.io/controller-uid' and 'batch.kubernetes.io/job-name' were introduced at
		// Kubernetes v1.27, which intend to replace the previous labels 'controller-uid' and 'job-name' respectively.
		// See https://github.com/kubernetes/kubernetes/pull/114930 for more details.
		delete(templateLabels, batchv1.ControllerUidLabel)
		delete(templateLabels, batchv1.JobNameLabel)

		err = unstructured.SetNestedStringMap(workload.Object, templateLabels, "spec", "template", "metadata", "labels")
		if err != nil {
			return err
		}
	}
	return nil
}

func removeDeploymentIrrelevantField(workload *unstructured.Unstructured) error {
	for _, annotation := range []string{utildeployment.RevisionAnnotation, utildeployment.RevisionHistoryAnnotation} {
		unstructured.RemoveNestedField(workload.Object, "metadata", "annotations", annotation)
	}

	return nil
}

// RemoveJobTTLSeconds removes the '.spec.ttlSecondsAfterFinished' from a Job.
// The reason for removing it is that the Job propagated by Karmada probably be automatically deleted
// from member clusters(by 'ttl-after-finished' controller in member clusters). That will cause a conflict if
// Karmada tries to re-create it. See https://github.com/karmada-io/karmada/issues/2197 for more details.
//
// It is recommended to enable the `ttl-after-finished` controller in the Karmada control plane.
// See https://karmada.io/docs/administrator/configuration/configure-controllers#ttl-after-finished for more details.
func RemoveJobTTLSeconds(workload *unstructured.Unstructured) {
	if workload.GetKind() == util.JobKind {
		unstructured.RemoveNestedField(workload.Object, "spec", "ttlSecondsAfterFinished")
	}
}

// removeJobIrrelevantField removes the irrelevant fields from Job (e.g. ManualSelector)
func removeJobIrrelevantField(workload *unstructured.Unstructured) error {
	job := &batchv1.Job{}
	err := helper.ConvertToTypedObject(workload, job)
	if err != nil {
		return err
	}
	if job.Spec.ManualSelector == nil || !*job.Spec.ManualSelector {
		if err = removeGenerateSelectorOfJob(workload); err != nil {
			return err
		}
	}
	return nil
}

// removeServiceAccountIrrelevantField removes the auto-generated secrets from ServiceAccount.
func removeServiceAccountIrrelevantField(workload *unstructured.Unstructured) error {
	secrets, exist, _ := unstructured.NestedSlice(workload.Object, "secrets")
	// If 'secrets' exists in ServiceAccount, remove the automatic generation secrets (e.g. default-token-xxx)
	if exist && len(secrets) > 0 {
		tokenPrefix := fmt.Sprintf("%s-token-", workload.GetName())
		for idx := 0; idx < len(secrets); idx++ {
			if strings.HasPrefix(secrets[idx].(map[string]interface{})["name"].(string), tokenPrefix) {
				secrets = append(secrets[:idx], secrets[idx+1:]...)
			}
		}
		_ = unstructured.SetNestedSlice(workload.Object, secrets, "secrets")
	}
	return nil
}

// removeServiceIrrelevantField removes member cluster specific fields from Service (e.g. clusterIP, clusterIPs)
func removeServiceIrrelevantField(workload *unstructured.Unstructured) error {
	// In the case spec.clusterIP is set to `None`, means user want a headless service,  then it shouldn't be removed.
	clusterIP, exist, _ := unstructured.NestedString(workload.Object, "spec", "clusterIP")
	if exist && clusterIP != corev1.ClusterIPNone {
		unstructured.RemoveNestedField(workload.Object, "spec", "clusterIP")
		unstructured.RemoveNestedField(workload.Object, "spec", "clusterIPs")
	}
	return nil
}

// removeSecretIrrelevantField removes the data and service-account uid annotation from service-account token secrets managed by member-cluster controller-manager
func removeSecretIrrelevantField(workload *unstructured.Unstructured) error {
	if secretType, exist, _ := unstructured.NestedString(workload.Object, "type"); exist && secretType == string(corev1.SecretTypeServiceAccountToken) {
		unstructured.RemoveNestedField(workload.Object, "metadata", "annotations", corev1.ServiceAccountUIDKey)
		_ = unstructured.SetNestedField(workload.Object, nil, "data")
	}
	return nil
}

func removePersistentVolumeClaimIrrelevantField(workload *unstructured.Unstructured) error {
	selectedNodeAnnotationPath := []string{"metadata", "annotations", storagevolume.AnnSelectedNode}
	if selectedNode, exist, _ := unstructured.NestedString(workload.Object, selectedNodeAnnotationPath...); exist && selectedNode != "" {
		unstructured.RemoveNestedField(workload.Object, selectedNodeAnnotationPath...)
	}
	return nil
}
