package server

import (
	"context"
	"fmt"
	"strconv"

	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/domain/workflow/dag"
	"github.com/tidbcloud/cluster-service-ng/pkg/runtime/actions"
	"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"
)

func (s *serverImpl) ApplySharedPool(ctx context.Context, sharedPoolId string, applyComponent *entity.TargetComponent, force bool) error {
	if sharedPoolId == "" {
		return status.Errorf(codes.InvalidArgument, "sharedPool id is required")
	}

	if applyComponent == nil {
		return status.Errorf(codes.InvalidArgument, "apply component is required")
	}

	if applyComponent.GetComponentType() == apiv1.ComponentType_COMPONENT_TYPE_UNSPECIFIED {
		return status.Errorf(codes.InvalidArgument, "apply component type is required")
	}

	var dagExecId string
	var dagSubmitted bool
	if err := s.transactioner.Transaction(ctx, func(ctx context.Context) error {
		// check if the sharedPool is locked
		unlockFunc, err := s.lock(ctx, sharedPoolId)
		if err != nil {
			tilog.Error(ctx, "failed to lock sharedPool", zap.String("sharedPoolId", sharedPoolId), zap.Error(err))
			return err
		}
		defer func() {
			if err := unlockFunc(); err != nil {
				tilog.Error(ctx, "failed to unlock sharedPool", zap.String("sharedPoolId", sharedPoolId), zap.Error(err))
			}
		}()

		// get shared first
		sharedPool, err := s.sharedPoolRepo.Load(ctx, sharedPoolId, false)
		if err != nil {
			if status.Code(err) != codes.NotFound {
				return status.Errorf(codes.Internal, "failed to load sharedPool: %s", err)
			}
			// not found, check if it's in creating
			return status.Errorf(codes.NotFound, "sharedPool %s not found", sharedPoolId)
		}
		if err := guardSharedPoolMutation(sharedPool, sharedPoolMutationBlockedWhilePaused); err != nil {
			return err
		}

		// merge sharedPool status from dag task
		err = s.mergeSharedPoolStatusFromDagTask(ctx, sharedPool)
		if err != nil {
			return status.Errorf(codes.Internal, "failed to merge sharedPool status from dag task: %s", err)
		}

		if applyComponent.GetId() == "" {
			tilog.Warn(ctx, "apply component id is empty, will use default id")
			applyComponent.Id = sharedPool.GetDefaultComponenId()
		}
		if isTiKVRemoteWorkerGuardedComponentType(applyComponent.GetComponentType()) {
			if err := s.guardTiKVRemoteWorkersTopologyStable(ctx, sharedPool); err != nil {
				return err
			}
		}

		// check if the sharedPool component is available
		comp := &entity.Component{
			Id:            applyComponent.GetId(),
			ComponentType: applyComponent.GetComponentType(),
		}
		err = checkSharedPoolCanBeModified(ctx, sharedPool, comp, force)
		if err != nil {
			return err
		}

		// Set requested template_version directly on SharedPool component
		// so downstream actions can read comp.GetTemplateVersion().
		if applyComponent.GetTemplateVersion() != "" {
			for _, c := range sharedPool.GetComponentGroups() {
				if c.GetId() == applyComponent.GetId() && c.GetComponentType() == applyComponent.GetComponentType() {
					c.TemplateVersion = applyComponent.GetTemplateVersion()
				}
			}
		}

		// set sharedPool phase to modifying
		sharedPool.Phase = apiv1.SharedPool_MODIFYING
		sharedPoolStr, err := protojson.Marshal(sharedPool)
		if err != nil {
			return status.Errorf(codes.Internal, "failed to marshal sharedPool: %s", err)
		}

		// start apply sharedpool dag
		args := actions.ActionArgs{
			workflow.SharedPoolIdKey:   actions.ActionArgString(sharedPool.GetId()),
			workflow.SharedPoolKey:     actions.ActionArgString(string(sharedPoolStr)),
			workflow.ComponentIdsKey:   actions.ActionArgString(comp.GetId()),
			workflow.ComponentTypesKey: actions.ActionArgString(entity.GetComponentTypeName(comp)),
			workflow.ForceKey:          actions.ActionArgString(strconv.FormatBool(force)),
			utils.TraceIdKey:           utils.GetTraceId(ctx),
		}

		dagExecId = utils.GeneratedDagExecId(ctx, fmt.Sprintf("apply-sharedpool-%s", sharedPool.GetId()))
		err = s.dagExecutor.Submit(ctx, dag.PlanApplyComponent, args, dagExecId)
		if err != nil {
			return status.Errorf(codes.Internal, "failed to submit apply sharedpool dag: %s", err)
		}
		dagSubmitted = true
		return nil
	}); err != nil {
		if dagSubmitted {
			if abortErr := s.dagExecutor.Abort(ctx, dagExecId); abortErr != nil {
				tilog.Error(ctx, "failed to abort dag after transaction failure",
					zap.String("dagExecId", dagExecId),
					zap.Error(abortErr))
			}
		}
		return err
	}

	return nil
}
