package common

import (
	"context"
	"fmt"

	"github.com/hashicorp/go-multierror"
	oplabel "github.com/pingcap/tidb-operator/pkg/apis/label"
	opv1alpha1 "github.com/pingcap/tidb-operator/pkg/apis/pingcap/v1alpha1"
	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/labels"
	"k8s.io/apimachinery/pkg/types"
	"sigs.k8s.io/controller-runtime/pkg/client"

	"github.com/tidbcloud/eks-provider/controllers/cluster/task"
	cloudv1 "github.com/tidbcloud/infra-api/api/v1"
	"github.com/tidbcloud/infra-provider/pkg/consts"
	"github.com/tidbcloud/infra-provider/pkg/deploy"
	"github.com/tidbcloud/infra-provider/pkg/k8s"
)

// TiDBGroupPVCleaner reclaims the persistent volumes during cluster deletion
type TiDBGroupPVCleaner struct {
	*task.BaseTask
}

func NewTiDBGroupPVCleaner(ctx *task.Context) deploy.Task {
	return &TiDBGroupPVCleaner{BaseTask: &task.BaseTask{Context: ctx}}
}

func (t *TiDBGroupPVCleaner) Name() string {
	return "tidbgroup.pv"
}

func (t *TiDBGroupPVCleaner) PostCreate(ctx context.Context) deploy.Result {
	// it's useless for tidb operator v2
	if t.Cluster.FeatureEnabled(consts.ClusterFeatureUseTiDBOperatorV2) {
		return deploy.Done()
	}

	if len(t.Cluster.Spec.ExtraGroups) == 0 {
		return deploy.Done()
	}

	extraGroups := t.Cluster.GetExtraGroups()

	var errs *multierror.Error
	for _, extraGroup := range extraGroups {
		if extraGroup.Deleted() {
			errs = multierror.Append(errs, t.EnsureZeroReplicasExtraGroupCleaned(ctx, &extraGroup))
			continue
		}
		errs = multierror.Append(errs, t.ReconcileExtraTiDBCluster(ctx, &extraGroup))
	}

	return deploy.ErrorOrDone(errs.ErrorOrNil())
}

func (t *TiDBGroupPVCleaner) Delete(ctx context.Context) deploy.Result {
	// all pvc in the namespaces have been deleted in the main cluster pvc cleaning
	return deploy.Done()
}

func (t *TiDBGroupPVCleaner) PostDelete(ctx context.Context) deploy.Result {
	return deploy.Done()
}

func (t *TiDBGroupPVCleaner) ReconcileExtraTiDBCluster(ctx context.Context, extraGroup *cloudv1.ExtraGroupCluster) error {
	remoteCli := t.EnvProvider.GetClient()
	extraGroupNamer := t.TidbGroupNamer(extraGroup.Spec).
		WithComponent(string(cloudv1.ComponentTiDB))

	tc := &opv1alpha1.TidbCluster{}

	key := types.NamespacedName{Name: extraGroupNamer.GetTiDBName(), Namespace: t.Namer.GetTiDBNamespace()}

	if err := remoteCli.Get(ctx, key, tc); err != nil {
		return err
	}

	ready := false
	for _, cond := range tc.Status.Conditions {
		if cond.Type == opv1alpha1.TidbClusterReady && cond.Status == corev1.ConditionTrue {
			ready = true
			break
		}
	}
	if !ready {
		return nil // do nothing
	}

	if err := t.deleteDeferDeletingExtraGroupPVCs(ctx, remoteCli, tc); err != nil {
		return fmt.Errorf("delete defer deleting ExtraGroupPVCs: %v", err)
	}
	return nil
}

// after scale in, the PVCs of the scaled-in pods are still in the cluster (tidb-operator will not delete them),
// we need to delete them.
func (t *TiDBGroupPVCleaner) deleteDeferDeletingExtraGroupPVCs(ctx context.Context, cli client.Client, tc *opv1alpha1.TidbCluster) error {
	pvcs := &corev1.PersistentVolumeClaimList{}
	ops := k8s.ListOpts[*corev1.PersistentVolumeClaim]{
		ObjectList: pvcs,
		Unpack:     func() []*corev1.PersistentVolumeClaim { return k8s.MapPtr(pvcs.Items) },
	}

	// list all PVCs in the namespace with `tidb.pingcap.com/pvc-defer-deleting` annotation
	list, err := ops.ListFilterBy(ctx, cli, func(pvc *corev1.PersistentVolumeClaim) bool {
		if _, ok := pvc.Annotations[oplabel.AnnPVCDeferDeleting]; ok {
			return true
		}
		return false
	}, t.pvcSelector(tc), client.InNamespace(t.Namer.GetTiDBNamespace()))
	if client.IgnoreNotFound(err) != nil {
		return err
	}

	for _, pvc := range list {
		t.Logger.Info("delete defer deleting ExtraGroupPVC", "pvc", pvc.Name)
		if err := cli.Delete(ctx, pvc); client.IgnoreNotFound(err) != nil {
			return err
		}
	}

	return nil
}

func (t *TiDBGroupPVCleaner) EnsureZeroReplicasExtraGroupCleaned(ctx context.Context, extraGroup *cloudv1.ExtraGroupCluster) error {
	remoteCli := t.EnvProvider.GetClient()
	extraGroupNamer := t.TidbGroupNamer(extraGroup.Spec).
		WithComponent(string(cloudv1.ComponentTiDB))

	tc := &opv1alpha1.TidbCluster{
		ObjectMeta: metav1.ObjectMeta{
			Name:      extraGroupNamer.GetTiDBName(),
			Namespace: t.Namer.GetTiDBNamespace(),
		},
	}
	key := types.NamespacedName{Name: tc.Name, Namespace: tc.Namespace}
	// if tc exists, skip extragroup cleaning
	if k8s.GetDeletedObject(ctx, remoteCli, key, tc) != nil {
		return nil
	}
	// list pvc, cleaning pvc
	pvcs := &corev1.PersistentVolumeClaimList{}
	ops := k8s.ListOpts[*corev1.PersistentVolumeClaim]{
		ObjectList: pvcs,
		Unpack:     func() []*corev1.PersistentVolumeClaim { return k8s.MapPtr(pvcs.Items) },
	}
	list, err := ops.ListFilterBy(ctx, remoteCli, func(_ *corev1.PersistentVolumeClaim) bool { return true }, t.pvcSelector(tc), client.InNamespace(t.Namer.GetTiDBNamespace()))
	if client.IgnoreNotFound(err) != nil {
		return err
	}
	for _, pvc := range list {
		t.Logger.Info("delete deleting ExtraGroupPVC", "pvc", pvc.Name)
		if err := remoteCli.Delete(ctx, pvc); client.IgnoreNotFound(err) != nil {
			return err
		}
	}
	if len(list) != 0 {
		return fmt.Errorf("extragroup persistent volume claim deleted, remain %d", len(list))
	}
	return nil
}

func (t *TiDBGroupPVCleaner) pvcSelector(tc *opv1alpha1.TidbCluster) client.MatchingLabelsSelector {
	// labels example:
	// app.kubernetes.io/component: tidb
	// app.kubernetes.io/instance: db-1234546-7412df2b
	// app.kubernetes.io/managed-by: tidb-operator
	// app.kubernetes.io/name: tidb-cluster
	// tidb.pingcap.com/cluster-id: "7299738809602123685"
	// tidb.pingcap.com/pod-name: db-1234546-7412df2b-tidb-0
	return client.MatchingLabelsSelector{
		Selector: labels.Set{
			consts.InstanceLabelKey: tc.Name,
		}.AsSelector(),
	}
}
