package resourcepool

import (
	"context"
	"math"
	"net/netip"
	"strconv"

	"github.com/samber/lo"
	clusterngv1beta "github.com/tidbcloud/cluster-service-ng/api/cluster-service-ng/v1beta"
	"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"
	regionrepo "github.com/tidbcloud/tidb-management-service/nextgen-global-service/domain/repository/region"
	"github.com/tidbcloud/tidb-management-service/nextgen-global-service/domain/repository/resourcepool"
	"github.com/tidbcloud/tidb-management-service/nextgen-global-service/domain/service/resourcepoolcapacity"
	"github.com/tidbcloud/tidb-management-service/utilities/bizerr"
	"github.com/tidbcloud/tidb-management-service/utilities/enum"
	"github.com/tidbcloud/tidb-management-service/utilities/pagination"
)

func (s *serviceImpl) ListOwnerResourcePools(ctx context.Context,
	request *globalngv1.ListOwnerResourcePoolsRequest,
) ([]*entity.ResourcePoolProfile, []*entity.ResourcePoolStrategy, bizerr.BizError) {
	ctx, span := tracer.Start(ctx, "Domain.ListOwnerResourcePools")
	defer span.End()

	if request == nil {
		return nil, nil, bizerr.New(bizerr.ErrCodeParams, "request cannot be nil")
	}
	if request.OwnerId == 0 {
		return nil, nil, bizerr.New(bizerr.ErrCodeParams, "OwnerId must be set")
	}
	if request.OwnerType != globalngv1.ResourcePool_ORGANIZATION {
		return nil, nil, bizerr.New(bizerr.ErrCodeParams, "OwnerType must be Organization")
	}

	servicePlans := make([]entity.ServicePlan, 0, 1)
	if request.ServicePlan != 0 {
		servicePlans = append(servicePlans, request.ServicePlan)
	}

	resourcePools, _, err := s.resourcePoolRepo.Query(ctx, &resourcepool.Filter{
		OwnerType:          int(request.OwnerType),
		OwnerIds:           []uint64{request.OwnerId},
		CloudProviderNames: request.ProviderIds,
		RegionNames:        request.RegionIds,
		ServicePlans:       servicePlans,
	})
	if err != nil {
		return nil, nil, bizerr.WrapError(err, "query resource pools failed")
	}

	resourcePoolProfiles, err := s.batchFullResourcePoolProfiles(ctx, resourcePools)
	if err != nil {
		return nil, nil, bizerr.WrapError(err, "batchFullResourcePoolProfiles failed")
	}
	if request.ServicePlan == globalcommonv1.ServicePlan_Premium_Reserved {
		return resourcePoolProfiles, []*entity.ResourcePoolStrategy{}, nil
	}

	strategies, err := s.resourcePoolStrategyRepo.Query(ctx, &resourcepool.StrategyFilter{
		OrganizationIds:    []uint64{request.OwnerId},
		CloudProviderNames: request.ProviderIds,
		RegionNames:        request.RegionIds,
		ServicePlans:       servicePlans,
	})
	if err != nil {
		return nil, nil, bizerr.WrapError(err, "query strategies failed")
	}

	return resourcePoolProfiles, strategies, err
}

func (s *serviceImpl) ListResourcePools(ctx context.Context,
	request *globalngv1.ListResourcePoolsRequest) (*globalngv1.ListResourcePoolsResponse, bizerr.BizError) {
	ctx, span := tracer.Start(ctx, "Domain.ListOwnerResourcePools")
	defer span.End()

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

	if request.View == globalngv1.ListResourcePoolsRequest_DETAIL && request.ShowDeleted {
		return nil, bizerr.New(bizerr.ErrCodeParams, "cannot show deleted resource pools in detail view")
	}

	var pageSize = int64(pagination.ListDefaultPageSize)
	if request.PageSize > 0 {
		pageSize = int64(request.PageSize)
	}
	if pageSize > int64(pagination.ListMaxPageSize) {
		return nil, bizerr.New(bizerr.ErrCodeParams, "page size exceeds the maximum limit")
	}

	cursorMode := request.GetPage() == 0 && !request.GetShowDeleted() &&
		(request.GetView() == globalngv1.ListResourcePoolsRequest_BASIC ||
			request.GetView() == globalngv1.ListResourcePoolsRequest_VIEW_UNSPECIFIED)
	if request.GetPageToken() != "" && !cursorMode {
		return nil, invalidResourcePoolListPageToken()
	}

	var offset int64
	if !cursorMode && request.GetPage() > 0 && request.GetPageSize() > 0 {
		offset = (int64(request.GetPage()) - 1) * pageSize
	}

	filter := &resourcepool.Filter{
		Ids:                request.ResourcePoolIds,
		Name:               entity.NormalizeResourcePoolName(request.ResourcePoolName),
		CloudProviderNames: request.ProviderIds,
		RegionNames:        request.RegionIds,
		OwnerType:          int(request.OwnerType),
		OwnerIds:           request.OwnerIds,
		ServicePlans:       []entity.ServicePlan{request.ServicePlan},
		LifecycleStates:    request.LifecycleStates,
		Offset:             offset,
		PageSize:           pageSize,
		ShowDeleted:        request.ShowDeleted,
	}
	if request.ServicePlan == 0 {
		filter.ServicePlans = nil
	}

	if request.CreatedAfter != nil {
		filter.CreatedAfter = request.CreatedAfter.AsTime()
	}
	if request.CreatedBefore != nil {
		filter.CreatedBefore = request.CreatedBefore.AsTime()
	}
	if cursorMode {
		cursor, err := parseResourcePoolListPageToken(request)
		if err != nil {
			return nil, err
		}
		filter.Cursor = cursor
		filter.PageSize++
	}

	rps, total, err := s.resourcePoolRepo.Query(ctx, filter)

	if err != nil {
		return nil, bizerr.WrapError(err, "query resource pools failed")
	}

	nextPageToken := ""
	if cursorMode && int64(len(rps)) > pageSize {
		rps = rps[:pageSize]
		last := rps[len(rps)-1]
		nextPageToken, err = buildResourcePoolListPageToken(request, resourcepool.Cursor{
			CreatedAt: last.GetCreateTime().AsTime(),
			ID:        last.GetId(),
		})
		if err != nil {
			return nil, err
		}
	}

	switch request.View {
	case globalngv1.ListResourcePoolsRequest_DETAIL:
		resourcePoolProfiles, err := s.batchFullResourcePoolProfiles(ctx, rps)
		if err != nil {
			return nil, bizerr.WrapError(err, "batchFullResourcePoolProfiles failed")
		}

		response := &globalngv1.ListResourcePoolsResponse{
			ResourcePools: resourcePoolProfiles,
			TotalSize:     int32(total),
			NextPageToken: nextPageToken,
		}
		return response, nil
	case globalngv1.ListResourcePoolsRequest_BASIC, globalngv1.ListResourcePoolsRequest_VIEW_UNSPECIFIED:
		resourcePoolIDs := lo.Map(rps, func(item *entity.ResourcePool, _ int) uint64 {
			return item.GetId()
		})
		instanceCounts, countErr := s.clusterRepo.CountLiveBySharedPoolIDs(ctx, resourcePoolIDs)
		if countErr != nil {
			return nil, bizerr.WrapError(countErr, "count resource pool instances")
		}
		dbRegionIDs := lo.Uniq(lo.FilterMap(rps, func(item *entity.ResourcePool, _ int) (uint64, bool) {
			return item.RegionId, item.RegionId != 0
		}))
		regionsByID := make(map[uint64]*entity.Region, len(dbRegionIDs))
		if len(dbRegionIDs) > 0 {
			regions, regionErr := s.regionRepo.ListRegions(ctx, regionrepo.Filter{
				DBRegionIds: dbRegionIDs,
				ShowDeleted: true,
			})
			if regionErr != nil {
				return nil, bizerr.WrapError(regionErr, "list resource pool regions")
			}
			regionsByID = lo.SliceToMap(regions, func(item *entity.Region) (uint64, *entity.Region) {
				return item.RegionID, item
			})
		}
		resourcePoolProfiles := make([]*globalngv1.ResourcePoolProfile, 0, len(rps))
		for _, item := range rps {
			item.InstanceCount = instanceCounts[item.GetId()]
			cloudRegion, ok := regionsByID[item.RegionId]
			if !ok || cloudRegion == nil || cloudRegion.Region == nil {
				return nil, bizerr.New(bizerr.ErrCodeRecordNotFound, "resource pool region not found")
			}
			resourcePoolProfiles = append(resourcePoolProfiles, &globalngv1.ResourcePoolProfile{
				ResourcePool: item.ResourcePool,
				Region:       cloudRegion.Region,
			})
		}

		response := &globalngv1.ListResourcePoolsResponse{
			ResourcePools: resourcePoolProfiles,
			TotalSize:     int32(total),
			NextPageToken: nextPageToken,
		}
		return response, nil
	default:
		return nil, bizerr.New(bizerr.ErrCodeParams, "invalid view type")
	}
}

func (s *serviceImpl) GetResourcePool(ctx context.Context,
	request *globalngv1.GetResourcePoolRequest) (*entity.ResourcePoolProfile, bizerr.BizError) {
	ctx, span := tracer.Start(ctx, "Domain.GetResourcePool")
	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, "ResourcePoolId must be set")
	}

	rps, _, err := s.resourcePoolRepo.Query(ctx, &resourcepool.Filter{
		Ids:         []uint64{request.ResourcePoolId},
		ShowDeleted: request.ShowDeleted,
	})

	if err != nil {
		return nil, bizerr.WrapError(err, "query resource pools failed")
	}
	if len(rps) == 0 {
		return nil, bizerr.New(bizerr.ErrCodeRecordNotFound, "resource pool not found")
	}
	if len(rps) > 1 {
		return nil, bizerr.New(bizerr.ErrCodeParams, "more than one resource pool found")
	}

	return s.fullResourcePoolProfile(ctx, rps[0])
}

func (s *serviceImpl) batchFullResourcePoolProfiles(ctx context.Context,
	rps []*entity.ResourcePool) ([]*entity.ResourcePoolProfile, bizerr.BizError) {
	if len(rps) == 0 {
		return []*entity.ResourcePoolProfile{}, nil
	}

	details := make([]*entity.ResourcePoolProfile, 0, len(rps))
	for _, rp := range rps {
		detail, err := s.fullResourcePoolProfile(ctx, rp)
		if err != nil {
			return nil, err
		}
		if detail != nil {
			details = append(details, detail)
		}
	}
	if len(details) > 0 {
		return details, nil
	}
	return nil, nil
}

func (s *serviceImpl) fullResourcePoolProfile(ctx context.Context,
	rp *entity.ResourcePool) (*entity.ResourcePoolProfile, bizerr.BizError) {
	if rp == nil {
		return &entity.ResourcePoolProfile{}, nil
	}

	profile := &entity.ResourcePoolProfile{
		ResourcePool: rp.ResourcePool,
	}

	if shouldManageResourcePoolStrategy(rp.GetServicePlan()) {
		if isSharedResourcePool(rp.ResourcePool) {
			// Essential_V2 shared pool: query all strategies referencing this pool.
			// org_id == 0 → global default (IsDefault = true)
			// org_id != 0 → collect into DefaultOrgs
			strategies, err := s.resourcePoolStrategyRepo.Query(ctx, &resourcepool.StrategyFilter{
				DefaultResourcePoolIds: []uint64{rp.Id},
			})
			if err != nil {
				return nil, err
			}
			for _, st := range strategies {
				if st.OrganizationId == 0 {
					profile.IsDefault = true
				} else {
					profile.DefaultOrgs = append(profile.DefaultOrgs, strconv.FormatUint(st.OrganizationId, 10))
				}
			}
		} else {
			// Org pool: check the org-scoped strategy as before.
			defaultExists, err := s.resourcePoolStrategyRepo.Query(ctx, &resourcepool.StrategyFilter{
				OrganizationIds:        []uint64{rp.OwnerId},
				DefaultResourcePoolIds: []uint64{rp.Id},
			})
			if err != nil {
				return nil, err
			}
			if len(defaultExists) > 0 {
				profile.IsDefault = true
			}
		}
	}

	cloudRegion, err := s.regionRepo.Get(ctx, rp.RegionId)

	if err != nil {
		return nil, bizerr.WrapError(err, "select region failed")
	}
	if cloudRegion == nil {
		return nil, bizerr.New(bizerr.ErrCodeRecordNotFound, "region not found")
	}

	profile.Region = cloudRegion.Region

	if rp.DeleteTime == nil {
		sharedPool, err := s.regionalCli.GetSharedPool(ctx, cloudRegion, &clusterngv1beta.GetSharedPoolRequest{
			SharedPoolId: strconv.FormatUint(rp.Id, 10),
		})

		if err != nil {
			if resourcePoolRuntimeMayBeAbsent(rp.GetLifecycleState()) &&
				resourcePoolRuntimeReadMayBeAbsent(err) {
				return s.fillResourcePoolInstances(ctx, profile, rp.Id)
			}
			if rp.GetServicePlan() == globalcommonv1.ServicePlan_BYOC {
				return nil, unavailableResourcePoolRuntime("get shared pool", err)
			}
			return nil, err
		}

		profile.Version = sharedPool.Version
		backfillResourcePoolZoneConfig(profile.GetResourcePool(), sharedPool.GetProvider().GetNodeZones())
		pdDeploymentMode, err := pdDeploymentModeFromRegional(sharedPool.GetPdDeploymentMode())
		if err != nil {
			return nil, err
		}
		profile.PdDeploymentMode = pdDeploymentMode
		enabledTikvRemoteWorkers, err := tikvRemoteWorkersFromRegional(sharedPool.GetEnabledTikvRemoteWorkers())
		if err != nil {
			return nil, err
		}
		profile.EnabledTikvRemoteWorkers = enabledTikvRemoteWorkers
		if rp.GetServicePlan() == globalcommonv1.ServicePlan_BYOC {
			if err := s.fillByocRuntimeProfile(ctx, cloudRegion, profile, sharedPool); err != nil {
				return nil, err
			}
		}

		for _, componentGroup := range sharedPool.GetComponentGroups() {
			switch componentGroup.ComponentType {
			case clusterngv1beta.ComponentType_TIKV:
				for _, componentStorage := range componentGroup.Storages {
					if componentStorage.Name == clusterngv1beta.Component_Storage_DATA {
						storageType, err := enum.FromString[globalngv1.ResourcePoolProfile_StorageType](componentStorage.Type)
						if err != nil {
							return nil, bizerr.WrapError(err, "invalid storage type")
						}
						profile.TikvDataStorageType = globalngv1.ResourcePoolProfile_StorageType(storageType)
					}
					if componentStorage.Name == clusterngv1beta.Component_Storage_RAFTLOG {
						storageType, err := enum.FromString[globalngv1.ResourcePoolProfile_StorageType](componentStorage.Type)
						if err != nil {
							return nil, bizerr.WrapError(err, "invalid storage type")
						}
						profile.TikvRaftlogStorageType = globalngv1.ResourcePoolProfile_StorageType(storageType)
					}
				}
			case clusterngv1beta.ComponentType_TIFLASH_WRITE:
				for _, componentStorage := range componentGroup.Storages {
					if componentStorage.Name == clusterngv1beta.Component_Storage_DATA {
						storageType, err := enum.FromString[globalngv1.ResourcePoolProfile_StorageType](componentStorage.Type)
						if err != nil {
							return nil, bizerr.WrapError(err, "invalid storage type")
						}
						profile.TiflashWnStorageType = globalngv1.ResourcePoolProfile_StorageType(storageType)
					}
				}
			case clusterngv1beta.ComponentType_TIFLASH_COMPUTE:
				for _, componentStorage := range componentGroup.Storages {
					if componentStorage.Name == clusterngv1beta.Component_Storage_DATA {
						storageType, err := enum.FromString[globalngv1.ResourcePoolProfile_StorageType](componentStorage.Type)
						if err != nil {
							return nil, bizerr.WrapError(err, "invalid storage type")
						}
						profile.TiflashCnStorageType = globalngv1.ResourcePoolProfile_StorageType(storageType)
					}
				}
			}
		}
	}

	return s.fillResourcePoolInstances(ctx, profile, rp.Id)
}

func (s *serviceImpl) fillByocRuntimeProfile(
	ctx context.Context,
	cloudRegion *entity.Region,
	profile *entity.ResourcePoolProfile,
	sharedPool *clusterngv1beta.SharedPool,
) bizerr.BizError {
	profile.ProvisionedDatabaseVcpus = nil
	runtimeMayBeAbsent := resourcePoolRuntimeMayBeAbsent(profile.GetResourcePool().GetLifecycleState())
	configuration, err := s.regionalCli.GetSharedPoolProvisionConfiguration(
		ctx,
		cloudRegion,
		&clusterngv1beta.GetSharedPoolProvisionConfigurationRequest{},
	)
	if err != nil {
		if runtimeMayBeAbsent && resourcePoolRuntimeReadMayBeAbsent(err) {
			return nil
		}
		return unavailableResourcePoolRuntime("get shared pool provision configuration", err)
	}
	o11yPrefix, parseErr := netip.ParsePrefix(configuration.GetO11YClusterCidr())
	if parseErr != nil || !o11yPrefix.Addr().Is4() {
		return unavailableResourcePoolRuntime("project observability cluster CIDR", parseErr)
	}
	o11yCIDR := o11yPrefix.Masked().String()
	profile.O11YClusterCidr = &o11yCIDR

	capacity, err := resourcepoolcapacity.Project(sharedPool.GetComponentGroups())
	if err != nil {
		// A resource pool can be visible in Global Service before the regional
		// runtime has published its complete component list. Keep returning the
		// durable/basic profile for lifecycle states where that is expected.
		if runtimeMayBeAbsent {
			return nil
		}
		return unavailableResourcePoolRuntime("project shared pool capacity", err)
	}
	profile.Capacity = capacity
	if runtimeMayBeAbsent {
		return nil
	}

	sharedPoolID := strconv.FormatUint(profile.GetResourcePool().GetId(), 10)
	usage, err := s.regionalCli.GetSharedPoolVcpusUsage(ctx, cloudRegion, &clusterngv1beta.GetSharedPoolVcpusUsageRequest{
		SharedPoolId: sharedPoolID,
	})
	if err != nil {
		return nil
	}
	if usage.GetVcpusUsed() > math.MaxInt64 {
		return unavailableResourcePoolRuntime("project shared pool vCPU usage", nil)
	}
	provisionedVcpus := int64(usage.GetVcpusUsed())
	profile.ProvisionedDatabaseVcpus = &provisionedVcpus
	return nil
}

func (s *serviceImpl) fillResourcePoolInstances(
	ctx context.Context,
	profile *entity.ResourcePoolProfile,
	resourcePoolID uint64,
) (*entity.ResourcePoolProfile, bizerr.BizError) {
	clusters, err := s.clusterRepo.Query(ctx, &cluster.Filter{SharedPoolIds: []uint64{resourcePoolID}})
	if err != nil {
		return nil, err
	}
	profile.Instances = lo.Map(clusters, func(item *entity.Cluster, _ int) *globalngv1.ResourcePoolProfile_Instance {
		return &globalngv1.ResourcePoolProfile_Instance{ClusterId: item.ClusterId}
	})
	profile.ResourcePool.InstanceCount = int64(len(profile.Instances))
	return profile, nil
}

func resourcePoolRuntimeMayBeAbsent(state globalngv1.ResourcePool_LifecycleState) bool {
	// FAILED is retained for compatibility with rows written before explicit
	// completion actions became the only lifecycle writers.
	switch state {
	case globalngv1.ResourcePool_CREATING,
		globalngv1.ResourcePool_FAILED,
		globalngv1.ResourcePool_PAUSING,
		globalngv1.ResourcePool_PAUSED,
		globalngv1.ResourcePool_RESUMING:
		return true
	default:
		return false
	}
}

func resourcePoolRuntimeReadMayBeAbsent(err bizerr.BizError) bool {
	if err == nil {
		return false
	}
	return err.ErrCode() == bizerr.ErrCodeRecordNotFound ||
		err.ErrCode() == bizerr.ErrCode(codes.Unavailable)
}

func unavailableResourcePoolRuntime(operation string, cause error) bizerr.BizError {
	if cause == nil {
		return bizerr.New(bizerr.ErrCode(codes.Unavailable), "resource pool runtime is unavailable during %s", operation)
	}
	return bizerr.New(
		bizerr.ErrCode(codes.Unavailable),
		"resource pool runtime is unavailable during %s",
		operation,
	).Chain("%s", cause.Error())
}

func backfillResourcePoolZoneConfig(resourcePool *globalngv1.ResourcePool, nodeZones []string) {
	if resourcePool == nil || len(nodeZones) == 0 {
		return
	}

	resourcePool.ZoneConfig = &globalngv1.ResourcePool_ZoneConfig{
		NodeZones: append([]string(nil), nodeZones...),
	}
	if len(nodeZones) == 1 {
		resourcePool.HighAvailabilityType = globalngv1.ResourcePool_ZONAL
		return
	}
	resourcePool.HighAvailabilityType = globalngv1.ResourcePool_REGIONAL
}
