package action

import (
	"fmt"

	"github.com/pingcap/tidb-operator/api/v2/core/v1alpha1"
	apiv1 "github.com/tidbcloud/cluster-service-ng/api/cluster-service-ng/v1beta"
	"github.com/tidbcloud/cluster-service-ng/domain/entity"
	"github.com/tidbcloud/cluster-service-ng/domain/workflow"
	"github.com/tidbcloud/cluster-service-ng/pkg/dataplane"
	"github.com/tidbcloud/cluster-service-ng/pkg/k8s"
	"github.com/tidbcloud/cluster-service-ng/pkg/runtime/actions"
	"github.com/tidbcloud/cluster-service-ng/pkg/storage/repo"
	"github.com/tidbcloud/cluster-service-ng/pkg/utils"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"go.uber.org/zap"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
	appsv1 "k8s.io/api/apps/v1"
	corev1 "k8s.io/api/core/v1"
	apierrors "k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"sigs.k8s.io/controller-runtime/pkg/client"
)

// this action delete related resource in dataplane k8s
// TODO(shenjun): change the action name to `clear-dataplane`
const ClearDataPlaneActionName = "clear-dataplane"

type ClearDataPlaneAction struct {
	dataplaneManager dataplane.Manager
	sharedPoolRepo   repo.SharedPoolRepo
}

func NewClearDataPlaneAction(dataplaneManager dataplane.Manager, sharedPoolRepo repo.SharedPoolRepo) actions.AsyncAction {
	return &ClearDataPlaneAction{
		dataplaneManager: dataplaneManager,
		sharedPoolRepo:   sharedPoolRepo,
	}
}

func (a *ClearDataPlaneAction) Name() string {
	return ClearDataPlaneActionName
}

func (a *ClearDataPlaneAction) Start(args actions.ActionArgs) error {
	// generate context
	ctx := utils.GenerateContext(args)
	tilog.Info(ctx, "clear dataplane action invoked")

	sharedPoolId, err := args.GetString(workflow.SharedPoolIdKey)
	if err != nil {
		return fmt.Errorf("failed to get sharedPool id: %w", err)
	}

	// load cluster block
	sharedPool, err := a.sharedPoolRepo.Load(ctx, sharedPoolId, true)
	if err != nil {
		if status.Code(err) == codes.NotFound {
			tilog.Info(ctx, "sharedPool not found, nothing to clear", zap.String("sharedPoolId", sharedPoolId))
			return nil // nothing to clear
		}
		return fmt.Errorf("failed to load sharedPool: %w", err)
	}

	remoteCli, err := a.dataplaneManager.GetClient(sharedPool.GetDataplaneName())
	if err != nil {
		// If dataplane is still creating or not found, there's nothing to clear
		if status.Code(err) == codes.NotFound {
			tilog.Info(ctx, "dataplane not found or still creating, nothing to clear",
				zap.String("dataplaneName", sharedPool.GetDataplaneName()))
			return nil
		}
		tilog.Error(ctx, "failed to get remote client for shared pool", zap.String("dataplaneName", sharedPool.GetDataplaneName()), zap.Error(err))
		return fmt.Errorf("failed to get remote client for shared pool: %w", err)
	}

	// first, delete the components
	for _, comp := range sharedPool.GetComponentGroups() {
		switch comp.GetComponentType() {
		case apiv1.ComponentType_TIDB:

			err = k8s.DeleteObject(ctx, remoteCli, &v1alpha1.TiDBGroup{
				ObjectMeta: metav1.ObjectMeta{
					Name:      entity.GetComponentGroupName(comp),
					Namespace: sharedPool.GetDataPlaneNamespace(),
				},
			})
			if err != nil {
				tilog.Error(ctx, "failed to delete tidb group", zap.String("component_id", comp.GetId()), zap.Error(err))
				return fmt.Errorf("failed to delete tidb group: %w", err)
			}

		case apiv1.ComponentType_TIKV:
			err = k8s.DeleteObject(ctx, remoteCli, &v1alpha1.TiKVGroup{
				ObjectMeta: metav1.ObjectMeta{
					Name:      entity.GetComponentGroupName(comp),
					Namespace: sharedPool.GetDataPlaneNamespace(),
				},
			})
			if err != nil {
				tilog.Error(ctx, "failed to delete tikv group", zap.String("component_id", comp.GetId()), zap.Error(err))
				return fmt.Errorf("failed to delete tikv group: %w", err)
			}

		case apiv1.ComponentType_PD:
			err = k8s.DeleteObject(ctx, remoteCli, &v1alpha1.PDGroup{
				ObjectMeta: metav1.ObjectMeta{
					Name:      entity.GetComponentGroupName(comp),
					Namespace: sharedPool.GetDataPlaneNamespace(),
				},
			})
			if err != nil {
				tilog.Error(ctx, "failed to delete pd group", zap.String("component_id", comp.GetId()), zap.Error(err))
				return fmt.Errorf("failed to delete pd group: %w", err)
			}
		case apiv1.ComponentType_PD_SCHEDULING:
			err = k8s.DeleteObject(ctx, remoteCli, &v1alpha1.SchedulingGroup{
				ObjectMeta: metav1.ObjectMeta{
					Name:      entity.GetComponentGroupName(comp),
					Namespace: sharedPool.GetDataPlaneNamespace(),
				},
			})
			if err != nil {
				tilog.Error(ctx, "failed to delete pd scheduling group", zap.String("component_id", comp.GetId()), zap.Error(err))
				return fmt.Errorf("failed to delete pd scheduling group: %w", err)
			}

		case apiv1.ComponentType_PD_TSO:
			err = k8s.DeleteObject(ctx, remoteCli, &v1alpha1.TSOGroup{
				ObjectMeta: metav1.ObjectMeta{
					Name:      entity.GetComponentGroupName(comp),
					Namespace: sharedPool.GetDataPlaneNamespace(),
				},
			})
			if err != nil {
				tilog.Error(ctx, "failed to delete pd tso group", zap.String("component_id", comp.GetId()), zap.Error(err))
				return fmt.Errorf("failed to delete pd tso group: %w", err)
			}

		case apiv1.ComponentType_TIFLASH_COMPUTE,
			apiv1.ComponentType_TIFLASH_WRITE:
			err = k8s.DeleteObject(ctx, remoteCli, &v1alpha1.TiFlashGroup{
				ObjectMeta: metav1.ObjectMeta{
					Name:      entity.GetComponentGroupName(comp),
					Namespace: sharedPool.GetDataPlaneNamespace(),
				},
			})
			if err != nil {
				tilog.Error(ctx, "failed to delete tiflash group", zap.String("component_id", comp.GetId()), zap.Error(err))
				return fmt.Errorf("failed to delete tiflash group: %w", err)
			}

		case apiv1.ComponentType_TIKV_WORKER,
			apiv1.ComponentType_COPROCESSOR_WORKER:
			err = k8s.DeleteObject(ctx, remoteCli, &v1alpha1.TiKVWorkerGroup{
				ObjectMeta: metav1.ObjectMeta{
					Name:      entity.GetComponentGroupName(comp),
					Namespace: sharedPool.GetDataPlaneNamespace(),
				},
			})
			if err != nil {
				tilog.Error(ctx, "failed to delete tikv worker", zap.String("component_id", comp.GetId()), zap.String("component_type", comp.GetComponentType().String()), zap.Error(err))
				return fmt.Errorf("failed to delete %s: %w", comp.GetComponentType().String(), err)
			}
		}
	}

	// delete deployment
	// get all namespace resource and delete them
	// first delete all resources in the namespace
	deployments := &appsv1.DeploymentList{}
	err = remoteCli.List(ctx, deployments, client.InNamespace(sharedPool.GetDataPlaneNamespace()))
	if err != nil {
		if apierrors.IsNotFound(err) {
			tilog.Info(ctx, "namespace not found when listing deployments, skip", zap.String("namespace", sharedPool.GetDataPlaneNamespace()))
		} else {
			tilog.Error(ctx, "failed to list deployments in dataplane namespace", zap.String("namespace", sharedPool.GetDataPlaneNamespace()), zap.Error(err))
			return fmt.Errorf("failed to list deployments in dataplane namespace: %w", err)
		}
	} else {
		for i := range deployments.Items {
			err = k8s.DeleteObject(ctx, remoteCli, &deployments.Items[i])
			if err != nil {
				tilog.Error(ctx, "failed to delete deployment in dataplane namespace", zap.String("namespace", sharedPool.GetDataPlaneNamespace()), zap.String("deployment", deployments.Items[i].Name), zap.Error(err))
				return fmt.Errorf("failed to delete deployment in dataplane namespace: %w", err)
			}
		}
	}

	// delete all sts
	statefulSets := &appsv1.StatefulSetList{}
	err = remoteCli.List(ctx, statefulSets, client.InNamespace(sharedPool.GetDataPlaneNamespace()))
	if err != nil {
		if apierrors.IsNotFound(err) {
			tilog.Info(ctx, "namespace not found when listing statefulsets, skip", zap.String("namespace", sharedPool.GetDataPlaneNamespace()))
		} else {
			tilog.Error(ctx, "failed to list statefulsets in dataplane namespace", zap.String("namespace", sharedPool.GetDataPlaneNamespace()), zap.Error(err))
			return fmt.Errorf("failed to list statefulsets in dataplane namespace: %w", err)
		}
	} else {
		for i := range statefulSets.Items {
			err = k8s.DeleteObject(ctx, remoteCli, &statefulSets.Items[i])
			if err != nil {
				tilog.Error(ctx, "failed to delete statefulset in dataplane namespace", zap.String("namespace", sharedPool.GetDataPlaneNamespace()), zap.String("statefulset", statefulSets.Items[i].Name), zap.Error(err))
				return fmt.Errorf("failed to delete statefulset in dataplane namespace: %w", err)
			}
		}
	}

	// delete pvc
	pvcs := &corev1.PersistentVolumeClaimList{}
	err = remoteCli.List(ctx, pvcs, client.InNamespace(sharedPool.GetDataPlaneNamespace()))
	if err != nil {
		if apierrors.IsNotFound(err) {
			tilog.Info(ctx, "namespace not found when listing pvcs, skip", zap.String("namespace", sharedPool.GetDataPlaneNamespace()))
		} else {
			tilog.Error(ctx, "failed to list pvcs in dataplane namespace", zap.String("namespace", sharedPool.GetDataPlaneNamespace()), zap.Error(err))
			return fmt.Errorf("failed to list pvcs in dataplane namespace: %w", err)
		}
	} else {
		for i := range pvcs.Items {
			err = k8s.DeleteObject(ctx, remoteCli, &pvcs.Items[i])
			if err != nil {
				tilog.Error(ctx, "failed to delete pvc in dataplane namespace", zap.String("namespace", sharedPool.GetDataPlaneNamespace()), zap.String("pvc", pvcs.Items[i].Name), zap.Error(err))
				return fmt.Errorf("failed to delete pvc in dataplane namespace: %w", err)
			}
		}
	}

	// finally, delete namespace
	err = k8s.DeleteObject(ctx, remoteCli, &corev1.Namespace{
		ObjectMeta: metav1.ObjectMeta{
			Name: sharedPool.GetDataPlaneNamespace(),
		},
	})
	if err != nil {
		tilog.Error(ctx, "failed to delete dataplane namespace", zap.Error(err))
		return fmt.Errorf("failed to delete dataplane namespace: %w", err)
	}

	return nil
}

func (a *ClearDataPlaneAction) CheckFinish(args actions.ActionArgs) (bool, error) {
	// generate context
	ctx := utils.GenerateContext(args)
	tilog.Info(ctx, "check clear dataplane action finish")

	sharedPoolId, err := args.GetString(workflow.SharedPoolIdKey)
	if err != nil {
		return false, fmt.Errorf("failed to get sharedPool id: %w", err)
	}

	// load cluster block
	sharedPool, err := a.sharedPoolRepo.Load(ctx, sharedPoolId, true)
	if err != nil {
		return false, fmt.Errorf("failed to load sharedPool: %w", err)
	}

	remoteCli, err := a.dataplaneManager.GetClient(sharedPool.GetDataplaneName())
	if err != nil {
		// If dataplane is still creating or not found, nothing was created so it's done
		if status.Code(err) == codes.NotFound {
			tilog.Info(ctx, "dataplane not found or still creating, nothing to clear, mark as done",
				zap.String("dataplaneName", sharedPool.GetDataplaneName()))
			return true, nil
		}
		tilog.Error(ctx, "failed to get remote client for shared pool", zap.String("dataplaneName", sharedPool.GetDataplaneName()), zap.Error(err))
		return false, fmt.Errorf("failed to get remote client for shared pool: %w", err)
	}

	// check if the namespace is deleted
	ns := &corev1.Namespace{}
	err = remoteCli.Get(ctx, client.ObjectKey{Name: sharedPool.GetDataPlaneNamespace()}, ns)
	if err == nil {
		tilog.Warn(ctx, "namespace still exists",
			zap.String("namespace", sharedPool.GetDataPlaneNamespace()),
			zap.String("status", string(ns.Status.Phase)),
			zap.String("sharedPoolId", sharedPoolId),
		)
		return false, nil // namespace still exists
	}
	if !apierrors.IsNotFound(err) {
		tilog.Error(ctx, "failed to get namespace",
			zap.String("namespace", sharedPool.GetDataPlaneNamespace()),
			zap.String("sharedPoolId", sharedPoolId),
			zap.Error(err),
		)
		return false, fmt.Errorf("failed to get namespace: %w", err)
	}
	tilog.Info(ctx, "namespace deleted",
		zap.String("namespace", sharedPool.GetDataPlaneNamespace()),
		zap.String("sharedPoolId", sharedPoolId),
	)
	return true, nil // namespace deleted
}
