package resourcepool

import (
	"context"
	"fmt"
	"time"

	"github.com/tidbcloud/tiinf/monitor/tilog"
	"go.uber.org/zap"

	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/resourcepool"
	"github.com/tidbcloud/tidb-management-service/utilities/bizerr"
)

type createResourcePoolFn func(
	ctx context.Context,
	pool entity.ResourcePool,
	poolName string,
	requiredRcu uint32,
	fixedTopology *globalngv1.FixedTopology,
) (*entity.ResourcePool, bizerr.BizError)

func (s *serviceImpl) GetOrCreateDefaultResourcePool(ctx context.Context,
	pool entity.ResourcePool, requiredRcu uint32) (*entity.ResourcePool, bizerr.BizError) {
	return s.getOrCreateDefaultResourcePool(ctx, pool, requiredRcu, nil)
}

// FindDefaultResourcePool returns the existing pool that the create path would
// use, without creating or switching pools. It returns nil when creation would
// need to create a new pool instead.
func (s *serviceImpl) FindDefaultResourcePool(ctx context.Context,
	pool entity.ResourcePool) (*entity.ResourcePool, bizerr.BizError) {
	if err := validateDefaultResourcePoolInput(pool); err != nil {
		return nil, err
	}
	if !shouldManageResourcePoolStrategy(pool.GetServicePlan()) {
		//nolint:nilnil // nil pool means no existing pool; the create path may create one.
		return nil, nil
	}

	strategy, err := s.resourcePoolStrategyRepo.GetByOrgProviderRegion(ctx, pool.OwnerId,
		pool.GetProviderId(), pool.GetRegionId(), pool.GetServicePlan())
	if err != nil {
		return nil, bizerr.WrapError(err, "query strategies failed")
	}
	if strategy.GetDefaultResourcePoolId() > 0 {
		defaultPool, loadErr := s.resourcePoolRepo.Load(ctx, strategy.DefaultResourcePoolId, false)
		if loadErr != nil {
			return nil, bizerr.WrapError(loadErr, "load default resource pool from strategy failed")
		}
		if defaultPool == nil {
			//nolint:nilnil // nil pool means no existing pool; the create path may create one.
			return nil, nil
		}
		if !shouldAutoCreateResourcePoolOnInstanceLimit(defaultPool.GetServicePlan()) {
			return defaultPool, nil
		}

		atLimit, limitErr := s.isResourcePoolAtInstanceLimit(ctx, defaultPool)
		if limitErr != nil {
			return nil, limitErr
		}
		if !atLimit {
			return defaultPool, nil
		}
		//nolint:nilnil // the default pool is full; the create path may select or create another one.
		return nil, nil
	}

	resourcePools, _, err := s.resourcePoolRepo.Query(ctx, &resourcepool.Filter{
		OwnerType:          int(pool.OwnerType),
		OwnerIds:           []uint64{pool.OwnerId},
		CloudProviderNames: []globalcommonv1.CloudProvider_Name{pool.GetProviderId()},
		RegionNames:        []globalcommonv1.Region_Name{pool.GetRegionId()},
		ServicePlans:       []entity.ServicePlan{pool.ServicePlan},
	})
	if err != nil {
		return nil, bizerr.WrapError(err, "query resource pools failed")
	}
	for _, candidate := range resourcePools {
		if candidate == nil {
			continue
		}
		if !shouldAutoCreateResourcePoolOnInstanceLimit(candidate.GetServicePlan()) {
			return candidate, nil
		}

		atLimit, limitErr := s.isResourcePoolAtInstanceLimit(ctx, candidate)
		if limitErr != nil {
			return nil, limitErr
		}
		if !atLimit {
			return candidate, nil
		}
	}
	//nolint:nilnil // nil pool means no existing pool; the create path may create one.
	return nil, nil
}

func (s *serviceImpl) CreateTopologyDrivenResourcePool(ctx context.Context,
	pool entity.ResourcePool, poolName string, fixedTopology *globalngv1.FixedTopology) (*entity.ResourcePool, bizerr.BizError) {
	return s.createDefaultResourcePool(ctx, pool, poolName, 0, fixedTopology)
}

func (s *serviceImpl) getOrCreateDefaultResourcePool(ctx context.Context,
	pool entity.ResourcePool, requiredRcu uint32, fixedTopology *globalngv1.FixedTopology) (*entity.ResourcePool, bizerr.BizError) {
	creator := s.createDefaultResourcePool
	return s.getOrCreateDefaultResourcePoolWithCreator(ctx, pool, requiredRcu, fixedTopology, creator)
}

func (s *serviceImpl) getOrCreateDefaultResourcePoolWithCreator(
	ctx context.Context,
	pool entity.ResourcePool,
	requiredRcu uint32,
	fixedTopology *globalngv1.FixedTopology,
	creator createResourcePoolFn,
) (*entity.ResourcePool, bizerr.BizError) {
	if creator == nil {
		return nil, bizerr.New(bizerr.ErrCodeParams, "resource pool creator cannot be nil")
	}

	if err := validateDefaultResourcePoolInput(pool); err != nil {
		return nil, err
	}
	if !shouldManageResourcePoolStrategy(pool.GetServicePlan()) {
		return creator(ctx, pool, "", requiredRcu, fixedTopology)
	}

	strategy, err := s.resourcePoolStrategyRepo.GetByOrgProviderRegion(ctx, pool.OwnerId,
		pool.GetProviderId(), pool.GetRegionId(), pool.GetServicePlan())
	if err != nil {
		return nil, bizerr.WrapError(err, "query strategies failed")
	}

	// default resource pool from strategy
	if strategy.GetDefaultResourcePoolId() > 0 {
		defaultPool, loadErr := s.resourcePoolRepo.Load(ctx, strategy.DefaultResourcePoolId, false)
		if loadErr != nil {
			return nil, bizerr.WrapError(loadErr, "load default resource pool from strategy failed")
		}

		return s.ensureResourcePoolCapacity(
			ctx,
			pool,
			defaultPool,
			requiredRcu,
			fixedTopology,
			strategy,
			creator,
		)
	}

	// no default strategy, try to find any resource pool that matches the criteria
	resourcePools, _, err := s.resourcePoolRepo.Query(ctx, &resourcepool.Filter{
		OwnerType: int(pool.OwnerType),
		OwnerIds:  []uint64{pool.OwnerId},
		CloudProviderNames: []globalcommonv1.CloudProvider_Name{
			pool.GetProviderId(),
		},
		RegionNames: []globalcommonv1.Region_Name{
			pool.GetRegionId(),
		},
		ServicePlans: []entity.ServicePlan{pool.ServicePlan},
	})
	if err != nil {
		return nil, bizerr.WrapError(err, "query resource pools failed")
	}

	if len(resourcePools) > 0 {
		for _, candidate := range resourcePools {
			availablePool, capErr := s.ensureResourcePoolCapacity(
				ctx,
				pool,
				candidate,
				requiredRcu,
				fixedTopology,
				nil,
				creator,
			)
			if capErr != nil {
				return nil, capErr
			}
			if availablePool != nil {
				return availablePool, nil
			}
		}

		return s.createPoolFromTemplate(
			ctx,
			pool,
			resourcePools[0],
			requiredRcu,
			fixedTopology,
			nil,
			creator,
		)
	}

	return creator(ctx, pool, "", requiredRcu, fixedTopology)
}

func (s *serviceImpl) ensureResourcePoolCapacity(
	ctx context.Context,
	requestPool entity.ResourcePool,
	candidate *entity.ResourcePool,
	requiredRcu uint32,
	fixedTopology *globalngv1.FixedTopology,
	strategy *entity.ResourcePoolStrategy,
	creator createResourcePoolFn,
) (*entity.ResourcePool, bizerr.BizError) {
	if candidate == nil {
		return nil, nil //nolint:nilnil // caller will continue fallback path
	}

	if !shouldAutoCreateResourcePoolOnInstanceLimit(candidate.GetServicePlan()) {
		return candidate, nil
	}

	atLimit, err := s.isResourcePoolAtInstanceLimit(ctx, candidate)
	if err != nil {
		return nil, err
	}
	if !atLimit {
		return candidate, nil
	}
	if strategy == nil {
		return nil, nil //nolint:nilnil // caller should continue scanning other candidates
	}

	return s.createPoolFromTemplate(ctx, requestPool, candidate, requiredRcu, fixedTopology, strategy, creator)
}

func (s *serviceImpl) createPoolFromTemplate(
	ctx context.Context,
	requestPool entity.ResourcePool,
	template *entity.ResourcePool,
	requiredRcu uint32,
	fixedTopology *globalngv1.FixedTopology,
	strategy *entity.ResourcePoolStrategy,
	creator createResourcePoolFn,
) (*entity.ResourcePool, bizerr.BizError) {
	if template == nil {
		return nil, bizerr.New(bizerr.ErrCodeParams, "template resource pool cannot be nil")
	}
	if template.ResourcePool == nil {
		return nil, bizerr.New(bizerr.ErrCodeParams, "template resource pool payload cannot be nil")
	}
	if creator == nil {
		return nil, bizerr.New(bizerr.ErrCodeParams, "resource pool creator cannot be nil")
	}

	newPoolInput := cloneResourcePoolForAutoCreate(template)
	poolName := s.nextAutoCreatedResourcePoolName(template.GetName(), requestPool)

	newPool, err := creator(ctx, newPoolInput, poolName, requiredRcu, fixedTopology)
	if err != nil {
		return nil, err
	}

	if switchErr := s.maybeSwitchDefaultResourcePool(ctx, strategy, template.GetId(), newPool.GetId()); switchErr != nil {
		return nil, switchErr
	}

	return newPool, nil
}

func (s *serviceImpl) maybeSwitchDefaultResourcePool(
	ctx context.Context,
	strategy *entity.ResourcePoolStrategy,
	oldPoolId uint64,
	newPoolId uint64,
) bizerr.BizError {
	if strategy == nil || strategy.GetId() == 0 {
		return nil
	}
	if oldPoolId == 0 || newPoolId == 0 || oldPoolId == newPoolId {
		return nil
	}
	if strategy.GetDefaultResourcePoolId() != oldPoolId {
		return nil
	}

	switched, err := s.resourcePoolStrategyRepo.SwitchDefaultResourcePool(ctx, strategy.GetId(), oldPoolId, newPoolId)
	if err != nil {
		return bizerr.WrapError(err, "failed to switch default resource pool to auto-created pool")
	}
	if switched {
		strategy.DefaultResourcePoolId = newPoolId
	}
	return nil
}

func (s *serviceImpl) isResourcePoolAtInstanceLimit(
	ctx context.Context,
	pool *entity.ResourcePool,
) (bool, bizerr.BizError) {
	if pool == nil {
		return false, bizerr.New(bizerr.ErrCodeParams, "resource pool cannot be nil")
	}
	if pool.GetInstanceLimit() == 0 {
		return false, nil
	}

	clusterCount, err := s.clusterRepo.CountBySharedPoolID(ctx, pool.GetId())
	if err != nil {
		return false, bizerr.WrapError(err, "count clusters by resource pool failed")
	}

	return clusterCount >= int64(pool.GetInstanceLimit()), nil
}

func validateDefaultResourcePoolInput(pool entity.ResourcePool) bizerr.BizError {
	if pool.OwnerId == 0 {
		return bizerr.New(bizerr.ErrCodeParams, "OwnerId must be set")
	}
	if pool.OwnerType != globalngv1.ResourcePool_ORGANIZATION {
		return bizerr.New(bizerr.ErrCodeParams, "OwnerType must be Organization")
	}
	if pool.ProviderId == globalcommonv1.CloudProvider_NAME_UNSPECIFIED {
		return bizerr.New(bizerr.ErrCodeParams, "ProviderId must be set")
	}
	if pool.GetRegionId() == globalcommonv1.Region_NAME_UNSPECIFIED {
		return bizerr.New(bizerr.ErrCodeParams, "RegionId must be set")
	}
	return nil
}

func (s *serviceImpl) createDefaultResourcePool(ctx context.Context,
	pool entity.ResourcePool, poolName string, requiredRcu uint32, fixedTopology *globalngv1.FixedTopology) (*entity.ResourcePool, bizerr.BizError) {
	if err := validateDefaultResourcePoolInput(pool); err != nil {
		return nil, err
	}

	// create a new default resource pool
	tilog.Info(ctx, "no default resource pool found, creating a new one",
		zap.Uint64("owner_id", pool.OwnerId),
		zap.String("provider_id", pool.GetProviderId().String()),
		zap.String("region_id", pool.GetRegionId().String()),
		zap.String("service_plan", pool.GetServicePlan().String()),
	)

	request := &globalngv1.CreateResourcePoolRequest{
		ResourcePool: &globalngv1.ResourcePool{
			Name:                  defaultResourcePoolName(pool, poolName),
			ProviderId:            pool.GetProviderId(),
			RegionId:              pool.GetRegionId(),
			OwnerType:             globalngv1.ResourcePool_ORGANIZATION,
			OwnerId:               pool.OwnerId,
			Cidr:                  pool.GetCidr(),
			ServicePlan:           pool.ServicePlan,
			InstanceLimit:         pool.InstanceLimit,
			K8SServiceCidr:        pool.GetK8SServiceCidr(),
			VcpusLimit:            pool.GetVcpusLimit(),
			PcaArn:                pool.GetPcaArn(),
			PlacementGroupEnabled: pool.GetPlacementGroupEnabled(),
			HighAvailabilityType:  pool.GetHighAvailabilityType(),
			ZoneConfig:            pool.GetZoneConfig(),
		},
		Demand: &globalngv1.CreateResourcePoolRequest_Demand{
			InitialRcu: defaultResourcePoolInitialRcu(requiredRcu),
		},
	}
	if pool.GetServicePlan() == globalcommonv1.ServicePlan_Premium_Reserved {
		request.TiflashMode = globalngv1.TiflashMode_TIFLASH_MODE_COLUMNAR
	}
	var resourcePool *entity.ResourcePoolProfile
	var err bizerr.BizError
	if fixedTopology != nil {
		resourcePool, err = s.createTopologyDrivenResourcePool(ctx, request, fixedTopology)
	} else {
		resourcePool, err = s.CreateResourcePool(ctx, request)
	}
	if err != nil {
		tilog.Error(ctx, "failed to create resource pool", zap.Error(err))
		return nil, err
	}

	return &entity.ResourcePool{
		ResourcePool: resourcePool.ResourcePool,
	}, nil
}

func cloneResourcePoolForAutoCreate(template *entity.ResourcePool) entity.ResourcePool {
	return entity.ResourcePool{
		ResourcePool: &globalngv1.ResourcePool{
			ProviderId:            template.GetProviderId(),
			RegionId:              template.GetRegionId(),
			OwnerType:             template.GetOwnerType(),
			OwnerId:               template.GetOwnerId(),
			Cidr:                  template.GetCidr(),
			ServicePlan:           template.GetServicePlan(),
			InstanceLimit:         template.GetInstanceLimit(),
			K8SServiceCidr:        template.GetK8SServiceCidr(),
			VcpusLimit:            template.GetVcpusLimit(),
			PcaArn:                template.GetPcaArn(),
			PlacementGroupEnabled: template.GetPlacementGroupEnabled(),
			HighAvailabilityType:  template.GetHighAvailabilityType(),
			ZoneConfig:            template.GetZoneConfig(),
		},
	}
}

func (s *serviceImpl) nextAutoCreatedResourcePoolName(currentName string, pool entity.ResourcePool) string {
	base := currentName
	if base == "" {
		base = defaultResourcePoolName(pool, "")
	}

	suffixNano := uint64(time.Now().UnixNano())
	if s.idGeneratorAdapter != nil {
		suffixNano = s.idGeneratorAdapter.MustNew()
	}

	return fmt.Sprintf("%s-%d", base, suffixNano)
}

func defaultResourcePoolName(pool entity.ResourcePool, customName string) string {
	if customName != "" {
		return customName
	}
	return fmt.Sprintf("default-%d-%s-%s", pool.OwnerId, pool.GetProviderId(), pool.GetRegionId())
}

func defaultResourcePoolInitialRcu(requiredRcu uint32) uint32 {
	return max(defaultInitialRcu, requiredRcu)
}
