package resourcepool

import (
	"context"
	"errors"
	"strconv"

	"github.com/tidbcloud/tiinf/monitor/tilog"
	"go.uber.org/zap"
	"google.golang.org/grpc/codes"

	globalcommonv1 "github.com/tidbcloud/tidb-management-service/api/spec/global/common/v1"
	globalngv1 "github.com/tidbcloud/tidb-management-service/api/spec/global/nextgen/v1"
	"github.com/tidbcloud/tidb-management-service/nextgen-global-service/domain/entity"
	"github.com/tidbcloud/tidb-management-service/nextgen-global-service/domain/repository/cluster"
	"github.com/tidbcloud/tidb-management-service/nextgen-global-service/domain/repository/region"
	"github.com/tidbcloud/tidb-management-service/nextgen-global-service/domain/resourcepoolpolicy"
	servicecommon "github.com/tidbcloud/tidb-management-service/nextgen-global-service/domain/service/common"
	"github.com/tidbcloud/tidb-management-service/nextgen-global-service/domain/service/workflow/common"
	"github.com/tidbcloud/tidb-management-service/utilities/bizerr"
	"github.com/tidbcloud/tidb-management-service/utilities/clusterevent"
	"github.com/tidbcloud/tidb-management-service/utilities/grpc/metadata"
	"github.com/tidbcloud/tidb-management-service/utilities/workflow"
)

func (s *serviceImpl) DeleteResourcePool(ctx context.Context,
	request *globalngv1.DeleteResourcePoolRequest,
) (*entity.ResourcePool, bizerr.BizError) {
	ctx, span := tracer.Start(ctx, "Domain.DeleteResourcePool")
	defer span.End()

	var resourcePool *entity.ResourcePool

	if request == nil {
		return nil, bizerr.New(bizerr.ErrCodeParams, "request cannot be nil")
	}

	if request.ResourcePoolId == 0 {
		return nil, bizerr.New(bizerr.ErrCodeParams, "resource pool id cannot be empty")
	}

	resourcePool, err := s.resourcePoolRepo.Load(ctx, request.ResourcePoolId, false)
	if err != nil {
		tilog.Error(ctx, "failed to load resource pool", zap.Error(err))
		return nil, err
	}

	if resourcePool == nil {
		return nil, bizerr.New(bizerr.ErrCodeRecordNotFound, "resource pool not found")
	}
	if lifecycleErr := servicecommon.RequireResourcePoolLifecycle(
		resourcePool,
		resourcepoolpolicy.OperationDeletePool,
	); lifecycleErr != nil {
		return nil, lifecycleErr
	}

	cloudRegion, err := s.regionRepo.SelectRegion(ctx, region.Filter{
		CloudProvider: resourcePool.ProviderId,
		RegionId:      resourcePool.ResourcePool.RegionId,
		TenantId:      resourcePoolTenantId(resourcePool.ResourcePool),
		ServicePlan:   resourcePool.ServicePlan,
	})
	if err != nil {
		tilog.Error(ctx, "failed to get region", zap.Error(err))
		return nil, err
	}

	if request.ValidateOnly {
		clustersUsingResourcePool, queryErr := s.clusterRepo.Query(ctx, &cluster.Filter{
			SharedPoolIds: []uint64{request.ResourcePoolId},
		})
		if queryErr != nil {
			return nil, queryErr
		}
		if len(clustersUsingResourcePool) > 0 {
			return nil, bizerr.New(
				bizerr.ErrCode(codes.FailedPrecondition),
				"resource pool is used by some clusters, cannot be deleted",
			)
		}
		return resourcePool, nil
	}

	terr := s.transaction.Transaction(ctx, func(ctx context.Context) error {
		lockedResourcePool, lockErr := s.resourcePoolRepo.LoadForUpdate(ctx, request.GetResourcePoolId(), false)
		if lockErr != nil {
			return lockErr
		}
		resourcePool = lockedResourcePool
		if lifecycleErr := servicecommon.RequireResourcePoolLifecycle(
			resourcePool,
			resourcepoolpolicy.OperationDeletePool,
		); lifecycleErr != nil {
			return lifecycleErr
		}

		clustersUsingResourcePool, queryErr := s.clusterRepo.Query(ctx, &cluster.Filter{
			SharedPoolIds: []uint64{request.ResourcePoolId},
		})
		if queryErr != nil {
			return queryErr
		}
		if len(clustersUsingResourcePool) > 0 {
			return bizerr.New(
				bizerr.ErrCode(codes.FailedPrecondition),
				"resource pool is used by some clusters, cannot be deleted",
			)
		}

		// 1. delete all system password
		// 2. delete resource pool from db
		// 3. if the resource pool is the default one for the org in the region, pick another one as default or delete the strategy if no other resource pool
		// 4. publish the "start to delete" event
		// 5. call regional server to delete resource pool
		return bizerr.OfNullable().BreakIf(func() bizerr.BizError {
			if err := s.passwordRepo.DeleteEncryptedPassword(
				ctx,
				request.GetResourcePoolId(),
				entity.SystemTidbId,
				entity.ClusterUserRoot,
			); err != nil {
				var be bizerr.BizError
				if errors.As(err, &be) && be.ErrCode() == bizerr.ErrCodeRecordNotFound {
					return nil
				}
				return err
			}
			return nil
		}).BreakIf(func() bizerr.BizError {
			if err := s.passwordRepo.DeleteEncryptedPassword(
				ctx,
				request.GetResourcePoolId(),
				entity.SystemTidbId,
				entity.ClusterUserMaintenanceAdmin,
			); err != nil {
				var be bizerr.BizError
				if errors.As(err, &be) && be.ErrCode() == bizerr.ErrCodeRecordNotFound {
					return nil
				}
				return err
			}
			return nil
		}).BreakIf(func() bizerr.BizError {
			if err := s.passwordRepo.DeleteEncryptedPassword(
				ctx,
				request.GetResourcePoolId(),
				entity.SystemTidbId,
				entity.ClusterUserPlatformAdmin,
			); err != nil {
				var be bizerr.BizError
				if errors.As(err, &be) && be.ErrCode() == bizerr.ErrCodeRecordNotFound {
					return nil
				}
				return err
			}
			return nil
		}).BreakIf(func() bizerr.BizError {
			if err := s.passwordRepo.DeleteEncryptedPassword(
				ctx,
				request.GetResourcePoolId(),
				entity.SystemTidbId,
				entity.ClusterUserCloudAdmin,
			); err != nil {
				var be bizerr.BizError
				if errors.As(err, &be) && be.ErrCode() == bizerr.ErrCodeRecordNotFound {
					return nil
				}
				return err
			}
			return nil
		}).BreakIf(func() bizerr.BizError {
			return s.resourcePoolRepo.Delete(ctx, request.ResourcePoolId)
		}).BreakIf(func() bizerr.BizError {
			if isSharedResourcePool(resourcePool.ResourcePool) {
				// For Essential_V2 shared pools, delete all org-default strategy
				// records that point to this pool (global defaults were already
				// blocked in the pre-transaction guard).
				if resourcePool.GetServicePlan() == globalcommonv1.ServicePlan_Essential_V2 {
					return s.deleteStrategiesByPoolId(ctx, request.ResourcePoolId)
				}
				return nil
			}
			if !shouldManageResourcePoolStrategy(resourcePool.GetServicePlan()) {
				return nil
			}
			_, innerErr := s.updateStrategyAfterPoolDeletion(
				ctx,
				cloudRegion,
				resourcePool.OwnerId,
				request.ResourcePoolId,
				resourcePool.ServicePlan,
			)
			return innerErr
		}).BreakIf(func() bizerr.BizError {
			eventAggr := &entity.ResourcePoolEventAggregation{
				ResourcePool: resourcePool,
			}
			events, evErr := eventAggr.DeletingEvent(ctx)
			if evErr != nil {
				return bizerr.WrapError(evErr, "build deleting event failed")
			}
			return s.eventCommitter.BatchWrite(ctx, clusterevent.TopicClusterEvent, events)
		}).BreakIf(func() bizerr.BizError {
			metadataJson, merr := metadata.MarshalRequestMetaFromCtx(ctx)
			if merr != nil {
				tilog.Warn(ctx, "failed to marshal request metadata", zap.Error(merr))
			}

			workflowErr := s.workflow.RunDagWithBizId(
				ctx,
				common.DeleteResourcePool,
				strconv.FormatUint(resourcePool.OwnerId, 10),
				"0", // nextgen cluster don't have project id
				strconv.FormatUint(resourcePool.Id, 10),
				map[string]string{
					workflow.KeyTraceId:      workflow.GetTraceId(ctx),
					workflow.KeyRequestMeta:  string(metadataJson),
					common.KeyResourcePoolId: strconv.FormatUint(resourcePool.Id, 10),
				},
			)
			if workflowErr != nil {
				return bizerr.WrapErrorWithCode(workflowErr, bizerr.ErrCodeUnknown, "create async workflow failed")
			}
			return nil
		}).Present()
	})

	if terr != nil {
		tilog.Error(ctx, "delete resource pool failed", zap.Error(terr))
		// if the terr is a bizerr.BizError
		var be bizerr.BizError
		if errors.As(terr, &be) {
			return nil, be
		}
		return nil, bizerr.WrapGormError(terr, "delete resource pool failed")
	}

	return resourcePool, nil
}
