package network

import (
	"context"
	"fmt"

	"github.com/pingcap/tidb-operator/pkg/apis/pingcap/v1alpha1"
	cloudv1 "github.com/tidbcloud/infra-api/api/v1"
	"github.com/tidbcloud/shared-provider/pkg/types"
	"github.com/tidbcloud/shared-provider/pkg/utils/labels"
	"github.com/tidbcloud/shared-provider/pkg/utils/namespace"
	corev1 "k8s.io/api/core/v1"
	apierrors "k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	apitypes "k8s.io/apimachinery/pkg/types"
	"sigs.k8s.io/controller-runtime/pkg/client"
)

const (
	reasonTiDBClusterExists = "TiDBClusterExists"
)

// Network is rescheduled means all resources has been cleaned.
// If reschedule is failed, reason will be annotated into network annotation,
// and dispatcher will never try to reschedule network again until issue is
// manually fixed.
// Alert for failed reschedule is needed although it is not a normal behavior.
func (r *Reconciler) Reschedule(ctx context.Context, net *cloudv1.Network) (bool, error) {
	if net.Annotations == nil {
		return false, nil
	}
	to, ok := net.Annotations[types.AnnoKeyReschedule]
	if !ok {
		return false, nil
	}
	abortReason, err := r.cleanAllResources(ctx, net)
	if err != nil {
		return false, fmt.Errorf("failed to clean resources for rescheduling: %v", err)
	}

	if abortReason != "" {
		net.Annotations[types.AnnoKeyRescheduleFailedReason] = abortReason
		delete(net.Annotations, types.AnnoKeyReschedule)
		if err := r.InfraClient.Update(ctx, net); err != nil {
			return false, err
		}

		return false, nil
	}

	delete(net.Annotations, types.AnnoKeyReschedule)
	net.Labels[labels.LabelKeyKubernetesCluster] = to

	if err := r.InfraClient.Update(ctx, net); err != nil {
		return false, err
	}

	r.Logger.Info("reschedule successfully", "network", net)

	return true, nil
}

func (r *Reconciler) cleanAllResources(ctx context.Context, net *cloudv1.Network) (string, error) {
	name := namespace.GetOwnedNamespaceNameFromNetwork(net)
	ns := &corev1.Namespace{}

	if err := r.Cache.Get(ctx, client.ObjectKey{Name: name}, ns); err != nil {
		if !apierrors.IsNotFound(err) {
			return "", fmt.Errorf("can't get namespace %s: %w", name, err)
		}
	} else {
		abort, reason, err := r.cleanInNamespaceResources(ctx, name)
		if err != nil {
			return "", err
		}
		if abort {
			return reason, nil
		}
	}

	if err := r.cleanUpImportResource(ctx, net); err != nil {
		return "", err
	}

	return "", nil
}

// TODO(liubo02): combine cleanup function
func (r *Reconciler) cleanInNamespaceResources(ctx context.Context, ns string) (bool, string, error) {
	cl := v1alpha1.TidbClusterList{}
	if err := r.Cache.List(ctx, &cl, client.InNamespace(ns)); err != nil {
		return false, "", err
	}
	if len(cl.Items) != 0 {
		return true, reasonTiDBClusterExists, nil
	}

	bl := v1alpha1.BackupList{}
	if err := r.Cache.List(ctx, &bl, client.InNamespace(ns)); err != nil {
		return false, "", err
	}
	for i := range bl.Items {
		b := &bl.Items[i]
		if err := r.updateBackupCleanPolicy(ctx, b); err != nil {
			return false, "", err
		}
	}

	if err := r.Client.Delete(ctx, &corev1.Namespace{
		ObjectMeta: metav1.ObjectMeta{
			Name: ns,
		},
	}); err != nil {
		if !apierrors.IsNotFound(err) {
			return false, "", fmt.Errorf("can't delete namespace %s: %w", ns, err)
		}
	}

	return false, "", nil
}

func (r *Reconciler) updateBackupCleanPolicy(ctx context.Context, b *v1alpha1.Backup) error {
	switch b.Spec.CleanPolicy {
	case v1alpha1.CleanPolicyTypeRetain:
		return nil
	case v1alpha1.CleanPolicyTypeDelete, v1alpha1.CleanPolicyTypeOnFailure:
		fallthrough
	default:
		r.Logger.Info("try to update backup clean policy", "backup", client.ObjectKeyFromObject(b))
		if err := r.Client.Patch(ctx, b, newUpdateBackupPatch()); err != nil {
			return err
		}
	}

	return nil
}

func newUpdateBackupPatch() client.Patch {
	patch := `{"metadata":{"finalizers":null},"spec":{"cleanPolicy":"Retain"}}`
	return client.RawPatch(apitypes.MergePatchType, []byte(patch))
}
