package resourcepool

import (
	"context"
	"errors"
	"strconv"

	clusterngv1beta "github.com/tidbcloud/cluster-service-ng/api/cluster-service-ng/v1beta"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"go.uber.org/zap"

	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/resourcepoolpolicy"
	servicecommon "github.com/tidbcloud/tidb-management-service/nextgen-global-service/domain/service/common"
	"github.com/tidbcloud/tidb-management-service/utilities/bizerr"
	"github.com/tidbcloud/tidb-management-service/utilities/clusterevent"
)

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

	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")
	}

	vcpuPolicyProvided := request.VcpusLimit != nil ||
		request.GetVcpuLimitMode() != globalngv1.ResourcePool_VCPU_LIMIT_MODE_UNSPECIFIED
	if request.InstanceLimit == 0 && !vcpuPolicyProvided {
		return nil, bizerr.New(bizerr.ErrCodeParams, "at least one of instance_limit or vcpus_limit must be provided")
	}

	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.OperationUpdatePoolLimits,
	); lifecycleErr != nil {
		return nil, lifecycleErr
	}

	oldInstanceLimit := resourcePool.InstanceLimit
	oldVcpusLimit := resourcePool.VcpusLimit
	targetInstanceLimit := oldInstanceLimit
	if request.InstanceLimit > 0 {
		targetInstanceLimit = request.InstanceLimit
	}
	instanceLimitChanged := targetInstanceLimit != oldInstanceLimit

	targetVcpusLimit := oldVcpusLimit
	targetVcpuLimitMode := resourcePool.GetVcpuLimitMode()
	if vcpuPolicyProvided {
		switch request.GetVcpuLimitMode() {
		case globalngv1.ResourcePool_LIMITED:
			if request.VcpusLimit == nil || request.GetVcpusLimit() == 0 {
				return nil, bizerr.New(bizerr.ErrCodeParams, "limited vcpu policy requires a positive vcpus_limit")
			}
			targetVcpusLimit = request.GetVcpusLimit()
			targetVcpuLimitMode = globalngv1.ResourcePool_LIMITED
		case globalngv1.ResourcePool_UNLIMITED:
			if request.VcpusLimit != nil {
				return nil, bizerr.New(bizerr.ErrCodeParams, "unlimited vcpu policy requires vcpus_limit to be omitted")
			}
			targetVcpusLimit = 0
			targetVcpuLimitMode = globalngv1.ResourcePool_UNLIMITED
		case globalngv1.ResourcePool_INHERITED:
			return nil, bizerr.New(bizerr.ErrCodeParams, "inherited vcpu policy cannot be set explicitly")
		case globalngv1.ResourcePool_VCPU_LIMIT_MODE_UNSPECIFIED:
			targetVcpusLimit = request.GetVcpusLimit()
			if targetVcpusLimit != 0 {
				targetVcpuLimitMode = globalngv1.ResourcePool_LIMITED
				break
			}
			targetVcpuLimitMode = globalngv1.ResourcePool_INHERITED
			rcuQuota, quotaErr := s.rcuQuotaRepo.Load(ctx, resourcePool.GetOwnerId(), resourcePool.GetServicePlan().String())
			if quotaErr != nil {
				return nil, bizerr.WrapError(quotaErr, "load rcu quota failed")
			}
			targetVcpusLimit = rcuQuota.GetVcpusLimitPerResourcePool()
		default:
			return nil, bizerr.New(bizerr.ErrCodeParams, "unsupported vcpu limit mode")
		}
	}

	var cloudRegion *entity.Region
	if vcpuPolicyProvided {
		var regionErr bizerr.BizError
		cloudRegion, regionErr = s.regionRepo.Get(ctx, resourcePool.RegionId)
		if regionErr != nil {
			tilog.Error(ctx, "failed to load region for shared pool vcpus limit update",
				zap.Error(regionErr),
				zap.Uint64("resourcePoolId", resourcePool.Id))
			return nil, regionErr
		}

		if targetVcpusLimit > 0 {
			currentVcpusUsed, usageErr := s.getSharedPoolVcpusUsage(ctx, cloudRegion, resourcePool.Id)
			if usageErr != nil {
				tilog.Error(ctx, "failed to get shared pool vcpus usage before update",
					zap.Error(usageErr),
					zap.Uint64("resourcePoolId", resourcePool.Id))
				return nil, usageErr
			}
			if targetVcpusLimit < currentVcpusUsed {
				return nil, bizerr.New(
					bizerr.ErrCodeParams,
					"the new vcpus limit %d is below current provisioned vcpu %d",
					targetVcpusLimit,
					currentVcpusUsed,
				)
			}
		}
	}

	resourcePool.InstanceLimit = targetInstanceLimit
	resourcePool.VcpusLimit = targetVcpusLimit
	resourcePool.VcpuLimitMode = targetVcpuLimitMode
	if request.GetValidateOnly() {
		return resourcePool, nil
	}

	if vcpuPolicyProvided {
		latest, loadErr := s.resourcePoolRepo.Load(ctx, resourcePool.GetId(), false)
		if loadErr != nil {
			return nil, loadErr
		}
		if lifecycleErr := servicecommon.RequireResourcePoolLifecycle(
			latest,
			resourcepoolpolicy.OperationUpdatePoolLimits,
		); lifecycleErr != nil {
			return nil, lifecycleErr
		}
		_, regionalErr := s.regionalCli.UpdateSharedPoolVcpusLimit(
			ctx,
			cloudRegion,
			&clusterngv1beta.UpdateSharedPoolVcpusLimitRequest{
				SharedPoolId: strconv.FormatUint(resourcePool.Id, 10),
				VcpusLimit:   targetVcpusLimit,
			},
		)
		if regionalErr != nil {
			tilog.Error(ctx, "failed to update shared pool vcpus limit in regional server",
				zap.Error(regionalErr),
				zap.Uint64("resourcePoolId", resourcePool.Id),
				zap.Uint64("vcpusLimit", resourcePool.GetVcpusLimit()))
			return nil, regionalErr
		}
	}

	persist := func(persistCtx context.Context) error {
		if s.transaction != nil {
			_, lockErr := s.resourcePoolRepo.LoadForUpdate(persistCtx, resourcePool.GetId(), false)
			if lockErr != nil {
				return lockErr
			}
		}
		if instanceLimitChanged {
			if updateErr := s.resourcePoolRepo.UpdateInstanceLimit(
				persistCtx,
				resourcePool.GetId(),
				targetInstanceLimit,
			); updateErr != nil {
				return updateErr
			}
		}
		if vcpuPolicyProvided {
			if updateErr := s.resourcePoolRepo.UpdateVcpuPolicy(
				persistCtx,
				resourcePool.GetId(),
				targetVcpuLimitMode,
				targetVcpusLimit,
			); updateErr != nil {
				return updateErr
			}
		}
		return nil
	}

	var persistErr error
	if s.transaction != nil {
		persistErr = s.transaction.Transaction(ctx, persist)
	} else {
		persistErr = persist(ctx)
	}
	if persistErr != nil {
		if vcpuPolicyProvided && cloudRegion != nil {
			_, revertErr := s.regionalCli.UpdateSharedPoolVcpusLimit(
				ctx,
				cloudRegion,
				&clusterngv1beta.UpdateSharedPoolVcpusLimitRequest{
					SharedPoolId: strconv.FormatUint(resourcePool.Id, 10),
					VcpusLimit:   oldVcpusLimit,
				},
			)
			if revertErr != nil {
				tilog.Error(ctx, "failed to revert shared pool vcpus limit in regional server after save failure",
					zap.Error(revertErr),
					zap.Uint64("resourcePoolId", resourcePool.Id),
					zap.Uint64("revertVcpusLimit", oldVcpusLimit))
			}
		}
		var businessErr bizerr.BizError
		if errors.As(persistErr, &businessErr) {
			return nil, businessErr
		}
		return nil, bizerr.WrapGormError(persistErr, "update resource pool limits")
	}
	resourcePool.VcpusLimit = targetVcpusLimit
	resourcePool.VcpuLimitMode = targetVcpuLimitMode

	// Publish event only when the instance limit was actually changed.
	if instanceLimitChanged {
		ev, evErr := entity.NewResourcePoolInstanceLimitUpdatedEvent(
			ctx,
			resourcePool,
			oldInstanceLimit,
			resourcePool.InstanceLimit,
		)
		if evErr != nil {
			tilog.Warn(ctx, "failed to build instance limit updated event", zap.Error(evErr))
		} else {
			if writeErr := s.eventCommitter.Write(ctx, clusterevent.TopicClusterEvent, ev); writeErr != nil {
				tilog.Warn(ctx, "failed to write instance limit updated event", zap.Error(writeErr))
			}
		}
	}

	return resourcePool, nil
}

func (s *serviceImpl) getSharedPoolVcpusUsage(
	ctx context.Context,
	cloudRegion *entity.Region,
	resourcePoolId uint64,
) (uint64, bizerr.BizError) {
	usageResp, err := s.regionalCli.GetSharedPoolVcpusUsage(
		ctx,
		cloudRegion,
		&clusterngv1beta.GetSharedPoolVcpusUsageRequest{
			SharedPoolId: strconv.FormatUint(resourcePoolId, 10),
		},
	)
	if err != nil {
		return 0, bizerr.WrapError(err, "get shared pool vcpus usage failed")
	}
	if usageResp == nil {
		return 0, bizerr.New(bizerr.ErrCodeUnknown, "received nil response for shared pool vcpus usage query")
	}
	return usageResp.GetVcpusUsed(), nil
}
