package helper

import (
	"context"
	"encoding/json"
	"fmt"
	"reflect"

	"k8s.io/apimachinery/pkg/api/meta"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
	"k8s.io/apimachinery/pkg/labels"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/runtime/schema"
	"k8s.io/apimachinery/pkg/util/sets"
	"k8s.io/client-go/util/retry"
	"k8s.io/klog/v2"
	"sigs.k8s.io/controller-runtime/pkg/client"

	workv1alpha1 "github.com/karmada-io/karmada/pkg/apis/work/v1alpha1"
	workv1alpha2 "github.com/karmada-io/karmada/pkg/apis/work/v1alpha2"
	"github.com/karmada-io/karmada/pkg/util"
	"github.com/karmada-io/karmada/pkg/util/names"
)

const (
	// FullyAppliedSuccessReason defines the success reason for the FullyApplied condition.
	FullyAppliedSuccessReason = "FullyAppliedSuccess"
	// FullyAppliedFailedReason defines the failure reason for the FullyApplied condition.
	FullyAppliedFailedReason = "FullyAppliedFailed"
	// FullyAppliedSuccessMessage defines the success message for the FullyApplied condition.
	FullyAppliedSuccessMessage = "All works have been successfully applied"
	// FullyAppliedFailedMessage defines the failure message for the FullyApplied condition.
	FullyAppliedFailedMessage = "Failed to apply all works, see status.aggregatedStatus for details"
)

// AggregateResourceBindingWorkStatus will collect all work statuses with current ResourceBinding objects,
// then aggregate status info to current ResourceBinding status.
func AggregateResourceBindingWorkStatus(c client.Client, binding *workv1alpha2.ResourceBinding, workload *unstructured.Unstructured) error {
	workList, err := GetWorksByLabelsSet(c, labels.Set{
		workv1alpha2.ResourceBindingReferenceKey: names.GenerateBindingReferenceKey(binding.Namespace, binding.Name),
	})
	if err != nil {
		klog.Errorf("Failed to get works by ResourceBinding(%s/%s): %v", binding.Namespace, binding.Name, err)
		return err
	}

	aggregatedStatuses, err := assembleWorkStatus(workList.Items, workload)
	if err != nil {
		return err
	}

	currentBindingStatus := binding.Status.DeepCopy()
	currentBindingStatus.AggregatedStatus = aggregatedStatuses
	meta.SetStatusCondition(&currentBindingStatus.Conditions, generateFullyAppliedCondition(binding.Spec, aggregatedStatuses))

	if reflect.DeepEqual(binding.Status, currentBindingStatus) {
		klog.V(4).Infof("New aggregatedStatuses are equal with old resourceBinding(%s/%s) AggregatedStatus, no update required.",
			binding.Namespace, binding.Name)
		return nil
	}

	return retry.RetryOnConflict(retry.DefaultRetry, func() (err error) {
		binding.Status = *currentBindingStatus
		updateErr := c.Status().Update(context.TODO(), binding)
		if updateErr == nil {
			return nil
		}

		updated := &workv1alpha2.ResourceBinding{}
		if err = c.Get(context.TODO(), client.ObjectKey{Namespace: binding.Namespace, Name: binding.Name}, updated); err == nil {
			// make a copy, so we don't mutate the shared cache
			binding = updated.DeepCopy()
		} else {
			klog.Errorf("failed to get updated binding %s/%s: %v", binding.Namespace, binding.Name, err)
		}

		return updateErr
	})
}

// AggregateClusterResourceBindingWorkStatus will collect all work statuses with current ClusterResourceBinding objects,
// then aggregate status info to current ClusterResourceBinding status.
func AggregateClusterResourceBindingWorkStatus(c client.Client, binding *workv1alpha2.ClusterResourceBinding, workload *unstructured.Unstructured) error {
	workList, err := GetWorksByLabelsSet(c, labels.Set{
		workv1alpha2.ClusterResourceBindingReferenceKey: names.GenerateBindingReferenceKey("", binding.Name),
	})
	if err != nil {
		klog.Errorf("Failed to get works by ClusterResourceBinding(%s): %v", binding.Name, err)
		return err
	}

	aggregatedStatuses, err := assembleWorkStatus(workList.Items, workload)
	if err != nil {
		return err
	}

	currentBindingStatus := binding.Status.DeepCopy()
	currentBindingStatus.AggregatedStatus = aggregatedStatuses
	meta.SetStatusCondition(&currentBindingStatus.Conditions, generateFullyAppliedCondition(binding.Spec, aggregatedStatuses))

	if reflect.DeepEqual(binding.Status, currentBindingStatus) {
		klog.Infof("New aggregatedStatuses are equal with old clusterResourceBinding(%s) AggregatedStatus, no update required.", binding.Name)
		return nil
	}

	return retry.RetryOnConflict(retry.DefaultRetry, func() (err error) {
		binding.Status = *currentBindingStatus
		updateErr := c.Status().Update(context.TODO(), binding)
		if updateErr == nil {
			return nil
		}

		updated := &workv1alpha2.ClusterResourceBinding{}
		if err = c.Get(context.TODO(), client.ObjectKey{Name: binding.Name}, updated); err == nil {
			// make a copy, so we don't mutate the shared cache
			binding = updated.DeepCopy()
		} else {
			klog.Errorf("failed to get updated binding %s/%s: %v", binding.Namespace, binding.Name, err)
		}

		return updateErr
	})
}

func generateFullyAppliedCondition(spec workv1alpha2.ResourceBindingSpec, aggregatedStatuses []workv1alpha2.AggregatedStatusItem) metav1.Condition {
	clusterNames := GetBindingClusterNames(spec.Clusters, spec.RequiredBy)
	if worksFullyApplied(aggregatedStatuses, clusterNames) {
		return util.NewCondition(workv1alpha2.FullyApplied, FullyAppliedSuccessReason, FullyAppliedSuccessMessage, metav1.ConditionTrue)
	}
	return util.NewCondition(workv1alpha2.FullyApplied, FullyAppliedFailedReason, FullyAppliedFailedMessage, metav1.ConditionFalse)
}

// assemble workStatuses from workList which list by selector and match with workload.
func assembleWorkStatus(works []workv1alpha1.Work, workload *unstructured.Unstructured) ([]workv1alpha2.AggregatedStatusItem, error) {
	statuses := make([]workv1alpha2.AggregatedStatusItem, 0)
	for _, work := range works {
		if !work.DeletionTimestamp.IsZero() {
			continue
		}

		identifierIndex, err := GetManifestIndex(work.Spec.Workload.Manifests, workload)
		if err != nil {
			klog.Errorf("Failed to get manifestIndex of workload in work.Spec.Workload.Manifests. Error: %v.", err)
			return nil, err
		}
		clusterName, err := names.GetClusterName(work.Namespace)
		if err != nil {
			klog.Errorf("Failed to get clusterName from work namespace %s. Error: %v.", work.Namespace, err)
			return nil, err
		}

		// if sync work to member cluster failed, then set status back to resource binding.
		var applied bool
		var appliedMsg string
		if cond := meta.FindStatusCondition(work.Status.Conditions, workv1alpha1.WorkApplied); cond != nil {
			switch cond.Status {
			case metav1.ConditionTrue:
				applied = true
			case metav1.ConditionUnknown:
				fallthrough
			case metav1.ConditionFalse:
				applied = false
				appliedMsg = cond.Message
			default: // should not happen unless the condition api changed.
				panic("unexpected status")
			}
		}
		if !applied {
			aggregatedStatus := workv1alpha2.AggregatedStatusItem{
				ClusterName:    clusterName,
				Applied:        applied,
				AppliedMessage: appliedMsg,
			}
			statuses = append(statuses, aggregatedStatus)
			continue
		}

		for _, manifestStatus := range work.Status.ManifestStatuses {
			equal, err := equalIdentifier(&manifestStatus.Identifier, identifierIndex, workload)
			if err != nil {
				return nil, err
			}
			if equal {
				aggregatedStatus := workv1alpha2.AggregatedStatusItem{
					ClusterName: clusterName,
					Status:      manifestStatus.Status,
					Applied:     applied,
				}
				statuses = append(statuses, aggregatedStatus)
				break
			}
		}
	}

	return statuses, nil
}

// GetManifestIndex get the index of clusterObj in manifest list, if not exist return -1.
func GetManifestIndex(manifests []workv1alpha1.Manifest, clusterObj *unstructured.Unstructured) (int, error) {
	for index, rawManifest := range manifests {
		manifest := &unstructured.Unstructured{}
		if err := manifest.UnmarshalJSON(rawManifest.Raw); err != nil {
			return -1, err
		}

		if manifest.GetAPIVersion() == clusterObj.GetAPIVersion() &&
			manifest.GetKind() == clusterObj.GetKind() &&
			manifest.GetNamespace() == clusterObj.GetNamespace() &&
			manifest.GetName() == clusterObj.GetName() {
			return index, nil
		}
	}

	return -1, fmt.Errorf("no such manifest exist")
}

func equalIdentifier(targetIdentifier *workv1alpha1.ResourceIdentifier, ordinal int, workload *unstructured.Unstructured) (bool, error) {
	groupVersion, err := schema.ParseGroupVersion(workload.GetAPIVersion())
	if err != nil {
		return false, err
	}

	if targetIdentifier.Ordinal == ordinal &&
		targetIdentifier.Group == groupVersion.Group &&
		targetIdentifier.Version == groupVersion.Version &&
		targetIdentifier.Kind == workload.GetKind() &&
		targetIdentifier.Namespace == workload.GetNamespace() &&
		targetIdentifier.Name == workload.GetName() {
		return true, nil
	}

	return false, nil
}

// worksFullyApplied checks if all works are applied according the scheduled result and collected status.
func worksFullyApplied(aggregatedStatuses []workv1alpha2.AggregatedStatusItem, targetClusters []string) bool {
	// short path: not scheduled
	if len(targetClusters) == 0 {
		return false
	}

	// short path: lack of status
	if len(targetClusters) != len(aggregatedStatuses) {
		return false
	}

	targetClusterSet := sets.String{}
	for i := range targetClusters {
		targetClusterSet.Insert(targetClusters[i])
	}

	for _, aggregatedSatusItem := range aggregatedStatuses {
		if !aggregatedSatusItem.Applied {
			return false
		}

		if !targetClusterSet.Has(aggregatedSatusItem.ClusterName) {
			return false
		}
	}

	return true
}

// IsResourceApplied checks whether resource has been dispatched to member cluster or not
func IsResourceApplied(workStatus *workv1alpha1.WorkStatus) bool {
	return meta.IsStatusConditionTrue(workStatus.Conditions, workv1alpha1.WorkApplied)
}

// IsWorkContains checks if the target resource exists in a work.
// Note: This function checks the Work object's status to detect the target resource, so the Work should be 'Applied',
// otherwise always returns false.
func IsWorkContains(workStatus *workv1alpha1.WorkStatus, targetResource schema.GroupVersionKind) bool {
	for _, manifestStatuses := range workStatus.ManifestStatuses {
		if targetResource.Group == manifestStatuses.Identifier.Group &&
			targetResource.Version == manifestStatuses.Identifier.Version &&
			targetResource.Kind == manifestStatuses.Identifier.Kind {
			return true
		}
	}
	return false
}

// BuildStatusRawExtension builds raw JSON by a status map.
func BuildStatusRawExtension(status map[string]interface{}) (*runtime.RawExtension, error) {
	statusJSON, err := json.Marshal(status)
	if err != nil {
		klog.Errorf("Failed to marshal status. Error: %v.", statusJSON)
		return nil, err
	}

	return &runtime.RawExtension{
		Raw: statusJSON,
	}, nil
}
