package action

import (
	"fmt"

	"github.com/google/uuid"
	dplanemgrv1 "github.com/tidbcloud/cluster-service-ng/api/dataplane-manager/v1"
	"github.com/tidbcloud/cluster-service-ng/domain/entity"
	"github.com/tidbcloud/cluster-service-ng/domain/workflow"
	"github.com/tidbcloud/cluster-service-ng/pkg/dataplanemgr"
	"github.com/tidbcloud/cluster-service-ng/pkg/nodegroup"
	"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"
	"google.golang.org/protobuf/encoding/protojson"
)

const (
	// DeleteNodeInstanceGroupActionName is the action name for deleting node instance group nodepools.
	DeleteNodeInstanceGroupActionName = "delete-node-instance-group-action"
)

// DeleteNodeInstanceGroupAction deletes nodepools (primary + fallback) for a node spec in a dataplane.
type DeleteNodeInstanceGroupAction struct {
	nodeGroupStateRepo     repo.DataplaneNodeInstanceGroupStateRepo
	nodeGroupLockRepo      repo.DataplaneNodeInstanceGroupLockRepo
	dataplaneServiceClient dplanemgrv1.DataPlaneServiceClient
}

// NewDeleteNodeInstanceGroupAction creates a new DeleteNodeInstanceGroupAction.
func NewDeleteNodeInstanceGroupAction(
	nodeGroupStateRepo repo.DataplaneNodeInstanceGroupStateRepo,
	nodeGroupLockRepo repo.DataplaneNodeInstanceGroupLockRepo,
	dataplaneClient dataplanemgr.Client,
) actions.Action {
	return &DeleteNodeInstanceGroupAction{
		nodeGroupStateRepo:     nodeGroupStateRepo,
		nodeGroupLockRepo:      nodeGroupLockRepo,
		dataplaneServiceClient: dataplaneClient.GetDataPlaneServiceClient(),
	}
}

// Name returns the action name.
func (a *DeleteNodeInstanceGroupAction) Name() string {
	return DeleteNodeInstanceGroupActionName
}

// Invoke executes the delete node instance group action.
func (a *DeleteNodeInstanceGroupAction) Invoke(args actions.ActionArgs) error {
	ctx := utils.GenerateContext(args)
	tilog.Info(ctx, "delete node instance group action invoked")

	dataplaneName, err := args.GetString(workflow.DataplaneIdKey)
	if err != nil {
		return fmt.Errorf("failed to get dataplane_id: %w", err)
	}

	// Parse NodeInstanceGroup from serialized data
	groupData, err := args.GetString(workflow.NodeInstanceGroupKey)
	if err != nil {
		return fmt.Errorf("failed to get node_instance_group: %w", err)
	}
	var group entity.NodeInstanceGroup
	if err := protojson.Unmarshal([]byte(groupData), &group); err != nil {
		return status.Errorf(codes.InvalidArgument, "failed to unmarshal node instance group: %v", err)
	}

	nodeSpec := entity.GetComponentNodeSpec(group.GetNodeSpec())
	primaryInstance := group.GetPrimaryInstanceType()
	fallbackInstances := group.GetFallbackInstances()

	if dataplaneName == "" {
		return status.Errorf(codes.InvalidArgument, "dataplane_id is required")
	}
	if err := nodegroup.ValidateSpec(nodeSpec); err != nil {
		return status.Errorf(codes.InvalidArgument, "invalid node_spec: %v", err)
	}
	if err := nodegroup.ValidateCloudInstanceType(primaryInstance); err != nil {
		return status.Errorf(codes.InvalidArgument, "invalid primary_instance: %v", err)
	}

	ownerID := fmt.Sprintf("%s-%s-%s", DeleteNodeInstanceGroupActionName, nodeSpec, uuid.New().String()[:8])
	lock, err := a.nodeGroupLockRepo.TryAcquire(ctx, dataplaneName, nodeSpec, primaryInstance, ownerID, DefaultLockLeaseDuration)
	if err != nil {
		tilog.Error(ctx, "failed to acquire lock",
			zap.String("dataplaneName", dataplaneName),
			zap.String("nodeSpec", nodeSpec),
			zap.String("ownerID", ownerID),
			zap.Error(err))
		return status.Errorf(codes.Unavailable, "failed to acquire lock: %v", err)
	}
	if lock == nil {
		tilog.Warn(ctx, "lock is held by another owner",
			zap.String("dataplaneName", dataplaneName),
			zap.String("nodeSpec", nodeSpec),
			zap.String("ownerID", ownerID))
		return status.Errorf(codes.Aborted, "lock is held by another owner, retry later")
	}

	defer func() {
		if releaseErr := a.nodeGroupLockRepo.Release(ctx, dataplaneName, nodeSpec, primaryInstance, ownerID); releaseErr != nil {
			tilog.Warn(ctx, "failed to release lock",
				zap.String("dataplaneName", dataplaneName),
				zap.String("nodeSpec", nodeSpec),
				zap.String("ownerID", ownerID),
				zap.Error(releaseErr))
		}
	}()

	tilog.Info(ctx, "deleting node instance group nodepools",
		zap.String("dataplaneName", dataplaneName),
		zap.String("nodeSpec", nodeSpec))

	nodePoolsResp, err := a.dataplaneServiceClient.ListNodePools(ctx, &dplanemgrv1.ListNodePoolsRequest{
		DataPlaneId: dataplaneName,
	})
	if err != nil {
		return status.Errorf(codes.Internal, "failed to list nodepools: %v", err)
	}

	// Parse nodeSpec to get CPU and Memory for matching dataplane default pools
	cpu, mem, err := nodegroup.ParseSpec(nodeSpec)
	if err != nil {
		return status.Errorf(codes.InvalidArgument, "invalid node spec %s: %v", nodeSpec, err)
	}

	instanceTypes := map[string]nodegroup.NodePoolRole{primaryInstance: nodegroup.NodePoolRolePrimary}
	for _, fallback := range fallbackInstances {
		if fallback == nil || fallback.GetInstanceType() == "" {
			continue
		}
		if _, exists := instanceTypes[fallback.GetInstanceType()]; exists {
			continue
		}
		instanceTypes[fallback.GetInstanceType()] = nodegroup.NodePoolRoleFallback
	}

	for _, np := range nodePoolsResp.GetNodePools() {
		if np == nil {
			continue
		}
		labels := np.GetLabels()
		isMatch := false

		if nodegroup.IsManagedByInstanceTypeManager(labels) {
			// Case 1: InstanceTypeManager managed pools - match by node-spec label
			if labels != nil && labels[nodegroup.LabelNodeSpec] == nodeSpec {
				isMatch = true
			}
		} else {
			// Case 2: Dataplane default pools - match by CPU/Memory
			if np.GetCpuCore() == cpu && np.GetMemoryGi() == mem {
				isMatch = true
			}
		}

		if !isMatch {
			continue
		}
		expectedRole, ok := instanceTypes[np.GetInstanceType()]
		if !ok {
			continue
		}
		if !nodegroup.NodePoolMatchesRole(np, expectedRole) {
			continue
		}
		req := &dplanemgrv1.DeleteNodePoolRequest{
			DataPlaneId: dataplaneName,
			NodePoolId:  np.GetName(),
		}
		if _, err := a.dataplaneServiceClient.DeleteNodePool(ctx, req); err != nil {
			if st, ok := status.FromError(err); ok && st.Code() == codes.NotFound {
				continue
			}
			return status.Errorf(codes.Internal, "failed to delete nodepool %s: %v", np.GetName(), err)
		}
	}

	state, err := a.nodeGroupStateRepo.LoadByDataplaneAndSpec(ctx, dataplaneName, nodeSpec, primaryInstance)
	if err != nil {
		return status.Errorf(codes.Internal, "failed to load node instance group state: %v", err)
	}
	if state != nil {
		if err := a.nodeGroupStateRepo.Delete(ctx, state.ID); err != nil {
			return status.Errorf(codes.Internal, "failed to delete node instance group state: %v", err)
		}
	}

	return nil
}
