package tidbxregion

import (
	"cmp"
	"context"
	"fmt"
	"slices"
	"strings"

	"github.com/samber/lo"
	accountsdk "github.com/tidbcloud/account/idl/pbgen/proto/account"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"go.uber.org/zap"
	"google.golang.org/protobuf/proto"
	_ "google.golang.org/protobuf/reflect/protoreflect"

	bffv1beta2 "github.com/tidbcloud/tidb-management-service/api/spec/bff/cluster_management/v1beta2"
	globalcommonv1 "github.com/tidbcloud/tidb-management-service/api/spec/global/common/v1"
	globaldedicatedv1 "github.com/tidbcloud/tidb-management-service/api/spec/global/dedicated/v1"
	globalserverlessv1 "github.com/tidbcloud/tidb-management-service/api/spec/global/serverless/v1"
	openapicommonv1beta1 "github.com/tidbcloud/tidb-management-service/api/spec/tidb_cloud_open_api/common/v1beta1"
	openapingv1beta2 "github.com/tidbcloud/tidb-management-service/api/spec/tidb_cloud_open_api/nextgen/v1beta2"
	"github.com/tidbcloud/tidb-management-service/tidb-mgmt-service/pkg/apimiddleware/bizerror"
	"github.com/tidbcloud/tidb-management-service/tidb-mgmt-service/pkg/common"
	"github.com/tidbcloud/tidb-management-service/utilities/bizerr"
	"github.com/tidbcloud/tidb-management-service/utilities/enum"
	"github.com/tidbcloud/tidb-management-service/utilities/grpc/metadata"
)

func (s *RegionServer) ListRegions(
	ctx context.Context,
	request *bffv1beta2.ListRegionsRequest,
) (*bffv1beta2.ListRegionsResponse, error) {
	// Extract request meta.
	reqMeta, err := metadata.ExtractRequestMetaWithCheckPermission(ctx)
	if err != nil {
		return nil, bizerr.WrapErrorWithCode(err, bizerr.ErrCodeParams, "failed to extract request metadata")
	}

	orgId := reqMeta.OrgID
	thirdProviders, err := s.accountCli.GetTenantThirdPartyAccountProviders(ctx, orgId)
	if err != nil {
		return nil, bizerr.WrapGrpcError(err, "failed to get third party account providers")
	}

	var allRegions []*openapicommonv1beta1.Region

	dedicatedRegions, err := s.listDedicatedRegions(ctx, request, orgId, thirdProviders)
	if err != nil {
		return nil, err
	}

	allRegions = append(allRegions, dedicatedRegions...)

	serverlessRegions, err := s.listServerlessRegions(ctx, request, orgId, thirdProviders)
	if err != nil {
		return nil, err
	}

	allRegions = append(allRegions, serverlessRegions...)

	nextgenRegions, err := s.listNextgenRegions(ctx, request)
	if err != nil {
		return nil, err
	}

	allRegions = append(allRegions, nextgenRegions...)

	// filter by cloud providers and service plans
	filterRegions := lo.Filter(allRegions, func(item *openapicommonv1beta1.Region, index int) bool {
		if item == nil {
			return false
		}
		// cloud providers not matched
		if len(request.CloudProviders) > 0 && !lo.Contains(request.CloudProviders, item.CloudProvider) {
			return false
		}

		// service plans not matched
		if len(request.ServicePlans) > 0 && len(lo.Intersect(item.ServicePlans, request.ServicePlans)) == 0 {
			return false
		}
		return true
	})
	for _, region := range filterRegions {
		setSupportedServicePlans(region)
	}

	resp := &bffv1beta2.ListRegionsResponse{}
	switch request.ViewMode {
	case bffv1beta2.ListRegionsRequest_SERVICE_PLAN_GROUP:
		resp.View = &bffv1beta2.ListRegionsResponse_ServicePlanGroupView{
			ServicePlanGroupView: s.servicePlanGroupView(ctx, filterRegions),
		}
	case bffv1beta2.ListRegionsRequest_FLAT:
		resp.View = &bffv1beta2.ListRegionsResponse_FlatView_{
			FlatView: s.flatView(ctx, filterRegions),
		}
	default:
		resp.View = &bffv1beta2.ListRegionsResponse_FlatView_{
			FlatView: s.flatView(ctx, filterRegions),
		}
	}

	return resp, nil
}

func (s *RegionServer) listDedicatedRegions(
	ctx context.Context,
	request *bffv1beta2.ListRegionsRequest,
	orgId uint64,
	thirdProviders []openapicommonv1beta1.Region_CloudProvider,
) ([]*openapicommonv1beta1.Region, error) {
	if len(request.ServicePlans) > 0 && !lo.Contains(request.ServicePlans, openapicommonv1beta1.ServicePlan_Dedicated) {
		tilog.Debug(ctx, "skip listing dedicated regions as service plans do not include DEDICATED")
		return []*openapicommonv1beta1.Region{}, nil
	}

	resp, err := s.dedicatedCli.ListCloudProviders(ctx, &globaldedicatedv1.ListCloudProvidersRequest{
		OrganizationId: orgId,
		IgnoreSpuCheck: true,
	})
	if err != nil {
		return nil, bizerror.NewInternal(err, "failed to list cloud providers")
	}

	ps := make([]*globalcommonv1.CloudProvider, 0)
	for _, p := range resp.GetCloudProviders() {
		provider := common.GlobalProviderToOpenApi(p.GetId())
		// if third party account providers exist
		if len(thirdProviders) > 0 && !lo.Contains(thirdProviders, provider) {
			continue
		}
		ps = append(ps, p)
	}

	if len(ps) == 0 {
		return []*openapicommonv1beta1.Region{}, nil
	}

	regions := make([]*openapicommonv1beta1.Region, 0)

	for _, p := range ps {
		for _, r := range p.GetRegions() {
			openapiName, openapiId,
				cloudProvider, err := common.BuildOpenApiRegionName(
				r.CloudProviderId.String(),
				r.Id.String(),
			)
			if err != nil {
				return nil, bizerror.NewInternal(err, "failed to build openapi region name")
			}

			// Filter by region status.
			if r.GetState() != globalcommonv1.Region_ACTIVE {
				tilog.Warn(ctx, "region is not active", zap.String("region_id", r.GetId().String()))
				continue
			}
			regions = append(regions, &openapicommonv1beta1.Region{
				Name:          openapiName,
				RegionId:      openapiId,
				CloudProvider: cloudProvider,
				DisplayName:   r.GetDisplayName(),
			})
		}
	}

	// set service plan to Dedicated
	for _, region := range regions {
		region.ServicePlans = []openapicommonv1beta1.ServicePlan{openapicommonv1beta1.ServicePlan_Dedicated}
	}
	return regions, nil
}

func (s *RegionServer) listServerlessRegions(
	ctx context.Context,
	request *bffv1beta2.ListRegionsRequest,
	orgId uint64,
	thirdProviders []openapicommonv1beta1.Region_CloudProvider,
) ([]*openapicommonv1beta1.Region, error) {
	if len(request.ServicePlans) > 0 &&
		!lo.Contains(request.ServicePlans, openapicommonv1beta1.ServicePlan_Starter) &&
		!lo.Contains(request.ServicePlans, openapicommonv1beta1.ServicePlan_Essential) {
		tilog.Debug(ctx, "skip listing serverless regions as service plans do not include STARTER or ESSENTIAL")
		return []*openapicommonv1beta1.Region{}, nil
	}
	var orgMeta *accountsdk.Org

	if len(thirdProviders) == 0 {
		// it's not a marketplace org, try checking if it's a alicloud whitelist org
		isAlicloudOrg, err := s.accountCli.IsHitFeatureFlag(
			ctx,
			&accountsdk.HitFeatureFlagReq{OrgId: orgId, FlagName: "enable_alibaba_cloud_orgs"},
		)
		if err != nil {
			tilog.Error(ctx, "failed to check alibaba cloud org feature flag", zap.Error(err))
		}
		if !isAlicloudOrg {
			orgMeta, _ = s.accountCli.GetOrgByID(ctx, orgId)
		}
	}

	// First, list all cloud providers.
	resp, err := s.serverlessCli.ListCloudProviders(ctx, &globalserverlessv1.CloudProviderServiceListRequest{
		OrganizationId: orgId,
	})
	if err != nil {
		return nil, err
	}

	if resp.Total == 0 {
		return []*openapicommonv1beta1.Region{}, nil
	}

	regionMap := make(map[string]*openapicommonv1beta1.Region)
	// Then, list all regions for each cloud provider.
	for _, p := range resp.GetCloudProviders() {
		for _, r := range p.GetRegions() {
			// check region status
			if r.State != globalcommonv1.Region_ACTIVE {
				tilog.Info(
					ctx,
					"region is not active",
					zap.String("region", r.Id.String()),
					zap.Uint64("org_id", orgId),
				)
				continue
			}

			// check region allow list if this region is not pubic
			if !r.IsPublic && len(r.GetOpenedTenantIds()) > 0 && !lo.Contains(r.GetOpenedTenantIds(), orgId) {
				tilog.Info(
					ctx,
					"region not pubilc",
					zap.String("region", r.Id.String()),
					zap.Uint64("org_id", orgId),
				)
				continue
			}

			cloudProvider, err := enum.FromString[openapicommonv1beta1.Region_CloudProvider](p.Id.String())
			if err != nil {
				return nil, bizerror.NewInternal(err, fmt.Sprintf("provider %s is not supported", p.Id.String()))
			}
			tilog.Info(
				ctx,
				"region",
				zap.Any("thirdProviders", thirdProviders),
				zap.Any("cloudProvider", cloudProvider),
			)

			// check third party account provider
			if len(thirdProviders) > 0 &&
				!lo.Contains(thirdProviders, openapicommonv1beta1.Region_CloudProvider(cloudProvider)) {
				tilog.Warn(
					ctx,
					"region not allowed by third party account provider",
					zap.String("region", r.Id.String()),
					zap.Uint64("org_id", orgId),
					zap.Any("third providers", thirdProviders),
				)
				continue
			}
			if orgMeta.GetCountry() == "JP" &&
				openapicommonv1beta1.Region_CloudProvider(cloudProvider) == openapicommonv1beta1.Region_alicloud {
				// disable alicloud region in JP org
				tilog.Warn(
					ctx,
					"alicloud region not allowed by org country",
					zap.Uint64("org_id", orgId),
					zap.Any("org_country", orgMeta.Country))
				continue
			}
			openapiId := fmt.Sprintf("%s-%s", strings.ToLower(p.GetId().String()), r.Id.String())
			if _, ok := regionMap[openapiId]; ok {
				regionMap[openapiId].ServicePlans = append(
					regionMap[openapiId].ServicePlans,
					common.CvtOpenAPIServicePlan(r.ServicePlan),
				)
				regionMap[openapiId].ServicePlans = lo.Uniq(regionMap[openapiId].ServicePlans)
				appendSupportedEngineType(
					regionMap[openapiId],
					common.CvtOpenAPIServicePlan(r.ServicePlan),
					cvtOpenAPIEngineType(r.GetEngineType()),
				)
			} else {
				regionMap[openapiId] = &openapicommonv1beta1.Region{
					Name:          fmt.Sprintf("regions/%s", openapiId),
					DisplayName:   r.DisplayName,
					CloudProvider: openapicommonv1beta1.Region_CloudProvider(cloudProvider),
					Provider:      lo.ToPtr(openapicommonv1beta1.Region_CloudProvider(cloudProvider).String()),
					RegionId:      openapiId,
					ServicePlans:  []openapicommonv1beta1.ServicePlan{common.CvtOpenAPIServicePlan(r.ServicePlan)},
				}
				appendSupportedEngineType(
					regionMap[openapiId],
					common.CvtOpenAPIServicePlan(r.ServicePlan),
					cvtOpenAPIEngineType(r.GetEngineType()),
				)
			}
		}
	}
	regions := lo.Values(regionMap)
	return regions, nil
}

func (s *RegionServer) listNextgenRegions(
	ctx context.Context,
	request *bffv1beta2.ListRegionsRequest,
) ([]*openapicommonv1beta1.Region, error) {
	if len(request.ServicePlans) > 0 &&
		!lo.Contains(request.ServicePlans, openapicommonv1beta1.ServicePlan_Premium) &&
		!lo.Contains(request.ServicePlans, openapicommonv1beta1.ServicePlan_BYOC) &&
		!lo.Contains(request.ServicePlans, openapicommonv1beta1.ServicePlan_Essential_V2) &&
		!lo.Contains(request.ServicePlans, openapicommonv1beta1.ServicePlan_Premium_Reserved) {
		tilog.Debug(ctx, "skip listing nextgen regions as service plans do not include PREMIUM, BYOC, ESSENTIAL_V2 or PREMIUM_RESERVED")
		return []*openapicommonv1beta1.Region{}, nil
	}
	resp, err := s.openAPINextgenRegionServer.ListRegions(ctx, &openapingv1beta2.ListRegionsRequest{})
	if err != nil {
		return nil, bizerr.WrapErrorWithCode(err, bizerr.ErrCodeUnknown, "failed to list nextgen regions")
	}

	return resp.GetRegions(), nil
}

func (s *RegionServer) servicePlanGroupView(
	ctx context.Context,
	regions []*openapicommonv1beta1.Region,
) *bffv1beta2.ListRegionsResponse_ServicePlanGroupedView {
	tilog.Debug(ctx, "grouping regions by service plans")
	servicePlanGroupRegions := map[openapicommonv1beta1.ServicePlan][]*openapicommonv1beta1.Region{}
	for _, region := range regions {
		if len(region.ServicePlans) == 0 {
			continue
		}
		for _, servicePlan := range region.ServicePlans {
			if _, ok := servicePlanGroupRegions[servicePlan]; !ok {
				servicePlanGroupRegions[servicePlan] = []*openapicommonv1beta1.Region{}
			}
			_, exists := lo.Find(servicePlanGroupRegions[servicePlan], func(item *openapicommonv1beta1.Region) bool {
				return item.RegionId == region.RegionId
			})
			if !exists {
				servicePlanGroupRegions[servicePlan] = append(servicePlanGroupRegions[servicePlan], region)
			}
		}
	}

	for _, groupRegions := range servicePlanGroupRegions {
		// Sort regions by region.cloud_provider first, then by region.name.
		slices.SortFunc(groupRegions, func(a, b *openapicommonv1beta1.Region) int {
			if c := cmp.Compare(a.CloudProvider, b.CloudProvider); c != 0 {
				return c
			}
			return cmp.Compare(a.Name, b.Name)
		})
	}

	result := &bffv1beta2.ListRegionsResponse_ServicePlanGroupedView{
		Items: []*bffv1beta2.ListRegionsResponse_ServicePlanGroup{},
	}
	for servicePlan, groupedRegions := range servicePlanGroupRegions {
		result.Items = append(result.Items, &bffv1beta2.ListRegionsResponse_ServicePlanGroup{
			ServicePlan: servicePlan,
			Regions:     groupedRegions,
		})
	}
	return result
}

func (s *RegionServer) flatView(
	ctx context.Context,
	regions []*openapicommonv1beta1.Region,
) *bffv1beta2.ListRegionsResponse_FlatView {
	tilog.Debug(ctx, "flat view regions")
	mergedRegions := make([]*openapicommonv1beta1.Region, 0)
	regionMap := make(map[string]*openapicommonv1beta1.Region)
	for _, region := range regions {
		if existingRegion, ok := regionMap[region.RegionId]; ok {
			// merge service plans
			existingRegion.ServicePlans = lo.Uniq(append(existingRegion.ServicePlans, region.ServicePlans...))
			mergeSupportedServicePlans(existingRegion, region)
			setSupportedServicePlans(existingRegion)
		} else {
			setSupportedServicePlans(region)
			regionMap[region.RegionId] = region
			mergedRegions = append(mergedRegions, region)
		}
	}

	// Sort regions by region.cloud_provider first, then by region.name.
	slices.SortFunc(mergedRegions, func(a, b *openapicommonv1beta1.Region) int {
		if c := cmp.Compare(a.CloudProvider, b.CloudProvider); c != 0 {
			return c
		}
		return cmp.Compare(a.Name, b.Name)
	})

	return &bffv1beta2.ListRegionsResponse_FlatView{
		Regions: mergedRegions,
	}
}

func mergeSupportedServicePlans(dst, src *openapicommonv1beta1.Region) {
	if dst == nil || src == nil {
		return
	}
	for _, srcInfo := range src.GetSupportedServicePlans() {
		if srcInfo == nil {
			continue
		}
		merged := false
		for _, dstInfo := range dst.GetSupportedServicePlans() {
			if dstInfo != nil && dstInfo.GetServicePlan() == srcInfo.GetServicePlan() {
				dstInfo.SupportedEngineTypes = lo.Uniq(append(dstInfo.GetSupportedEngineTypes(), srcInfo.GetSupportedEngineTypes()...))
				merged = true
				break
			}
		}
		if merged {
			continue
		}
		dst.SupportedServicePlans = append(dst.SupportedServicePlans, &openapicommonv1beta1.ServicePlanInfo{
			ServicePlan:          srcInfo.GetServicePlan(),
			DisplayName:          srcInfo.GetDisplayName(),
			SupportedEngineTypes: lo.Uniq(srcInfo.GetSupportedEngineTypes()),
		})
	}
}

func setSupportedServicePlans(region *openapicommonv1beta1.Region) {
	if region == nil {
		return
	}
	planInfoMap := make(map[openapicommonv1beta1.ServicePlan]*openapicommonv1beta1.ServicePlanInfo)
	for _, info := range region.GetSupportedServicePlans() {
		if info == nil {
			continue
		}
		planInfoMap[info.GetServicePlan()] = &openapicommonv1beta1.ServicePlanInfo{
			ServicePlan:          info.GetServicePlan(),
			DisplayName:          info.GetDisplayName(),
			SupportedEngineTypes: lo.Uniq(info.GetSupportedEngineTypes()),
		}
	}
	region.SupportedServicePlans = lo.Map(region.GetServicePlans(), func(
		plan openapicommonv1beta1.ServicePlan,
		_ int,
	) *openapicommonv1beta1.ServicePlanInfo {
		info, ok := planInfoMap[plan]
		if !ok {
			info = &openapicommonv1beta1.ServicePlanInfo{
				ServicePlan:          plan,
				SupportedEngineTypes: defaultSupportedEngineTypes(plan),
			}
		}
		return &openapicommonv1beta1.ServicePlanInfo{
			ServicePlan:          plan,
			DisplayName:          servicePlanDisplayName(plan),
			SupportedEngineTypes: lo.Uniq(info.GetSupportedEngineTypes()),
		}
	})
}

func appendSupportedEngineType(
	region *openapicommonv1beta1.Region,
	plan openapicommonv1beta1.ServicePlan,
	engineType openapicommonv1beta1.EngineType,
) {
	if region == nil || engineType == openapicommonv1beta1.EngineType_ENGINE_TYPE_UNSPECIFIED {
		return
	}
	for _, info := range region.GetSupportedServicePlans() {
		if info != nil && info.GetServicePlan() == plan {
			info.SupportedEngineTypes = lo.Uniq(append(info.GetSupportedEngineTypes(), engineType))
			return
		}
	}
	region.SupportedServicePlans = append(region.SupportedServicePlans, &openapicommonv1beta1.ServicePlanInfo{
		ServicePlan:          plan,
		DisplayName:          servicePlanDisplayName(plan),
		SupportedEngineTypes: []openapicommonv1beta1.EngineType{engineType},
	})
}

func cvtOpenAPIEngineType(engineType globalcommonv1.Region_EngineType) openapicommonv1beta1.EngineType {
	switch engineType {
	case globalcommonv1.Region_POSTGRESQL_COMPATIBLE:
		return openapicommonv1beta1.EngineType_POSTGRESQL_COMPATIBLE
	case globalcommonv1.Region_ENGINE_TYPE_UNSPECIFIED, globalcommonv1.Region_MYSQL_COMPATIBLE:
		return openapicommonv1beta1.EngineType_MYSQL_COMPATIBLE
	default:
		return openapicommonv1beta1.EngineType_ENGINE_TYPE_UNSPECIFIED
	}
}

func defaultSupportedEngineTypes(plan openapicommonv1beta1.ServicePlan) []openapicommonv1beta1.EngineType {
	switch plan {
	case openapicommonv1beta1.ServicePlan_Starter, openapicommonv1beta1.ServicePlan_Essential:
		return []openapicommonv1beta1.EngineType{openapicommonv1beta1.EngineType_MYSQL_COMPATIBLE}
	default:
		return nil
	}
}

func servicePlanDisplayName(plan openapicommonv1beta1.ServicePlan) string {
	return GetDisplayName(plan)
}

func GetDisplayName(val openapicommonv1beta1.ServicePlan) string {
	entry := val.Descriptor().Values().ByNumber(val.Number())
	if entry == nil {
		return ""
	}

	options := entry.Options()

	if proto.HasExtension(options, openapicommonv1beta1.E_DisplayName) {
		ext := proto.GetExtension(options, openapicommonv1beta1.E_DisplayName)
		if str, ok := ext.(string); ok {
			return str
		}
	}

	return string(entry.Name())
}
