package region

import (
	"cmp"
	"context"
	"slices"

	"github.com/samber/lo"

	globalcommonv1 "github.com/tidbcloud/tidb-management-service/api/spec/global/common/v1"
	globalngv1 "github.com/tidbcloud/tidb-management-service/api/spec/global/nextgen/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"
)

const (
	// ListRegionsPageSize is the default page size for listing regions.
	ListRegionsPageSize = 20
)

func (t serverImpl) ListRegions(
	ctx context.Context,
	req *openapingv1beta2.ListRegionsRequest,
) (*openapingv1beta2.ListRegionsResponse, error) {
	ctx, span := tracer.Start(ctx, "ListRegions")
	defer span.End()

	// Extract request meta.
	reqMeta, err := metadata.ExtractRequestMetaWithCheckPermission(ctx)
	if err != nil {
		return nil, bizerr.WrapErrorWithCode(err, bizerr.ErrCodeParams, "failed to extract request metadata")
	}

	pName, err := enum.FromString[openapicommonv1beta1.Region_CloudProvider](req.GetCloudProvider())
	if err != nil {
		return nil, bizerr.WrapErrorWithCode(err, bizerr.ErrCodeParams, "invalid cloud provider")
	}

	orgId := reqMeta.OrgID
	resp, err := t.nextgenCli.ListRegions(ctx, &globalngv1.ListRegionsRequest{
		OrganizationId:  orgId,
		ServicePlan:     common.CvtGlobalServicePlan(req.GetServicePlan()),
		CloudProviderId: globalcommonv1.CloudProvider_Name(pName),
		State:           globalcommonv1.Region_ACTIVE,
	})
	if err != nil {
		return nil, bizerror.NewInternal(err, "failed to list regions")
	}

	regionMap := make(map[string]*openapicommonv1beta1.Region)
	for _, region := range resp.Regions {
		openapiName, openapiId, cloudProvider, err := common.BuildOpenApiRegionName(
			region.CloudProviderId.String(),
			region.Id.String(),
		)
		if err != nil {
			return nil, bizerror.NewInternal(err, "failed to build openapi region name")
		}

		servicePlans := cvtRegionServicePlans(region)
		if _, exists := regionMap[openapiId]; exists {
			regionMap[openapiId].ServicePlans = append(
				regionMap[openapiId].ServicePlans,
				servicePlans...,
			)
			regionMap[openapiId].ServicePlans = lo.Uniq(regionMap[openapiId].ServicePlans)
		} else {
			regionMap[openapiId] = &openapicommonv1beta1.Region{
				Name:          openapiName,
				RegionId:      openapiId,
				CloudProvider: cloudProvider,
				DisplayName:   region.GetDisplayName(),
				ServicePlans:  servicePlans,
			}
		}
	}
	regions := lo.Values(regionMap)

	thirdProviders, err := t.accountCli.GetTenantThirdPartyAccountProviders(ctx, orgId)
	if err != nil {
		return nil, bizerr.WrapGrpcError(err, "failed to get third party account providers")
	}
	if len(thirdProviders) > 0 {
		regions = lo.Filter(regions, func(r *openapicommonv1beta1.Region, _ int) bool {
			return lo.Contains(thirdProviders, r.GetCloudProvider())
		})
	}

	// Sort regions by region.cloud_provider first, then by region.name.
	slices.SortFunc(regions, 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)
	})

	total := int32(len(regions))
	pagedRegions, nextToken, err := common.LocalPagination(regions, req, ListRegionsPageSize, common.ListMaxPageSize)
	if err != nil {
		return nil, bizerror.NewInternal(err, "failed to paginate regions")
	}

	return &openapingv1beta2.ListRegionsResponse{
		Regions:       pagedRegions,
		TotalSize:     total,
		NextPageToken: nextToken,
	}, nil
}

func (t serverImpl) ListOpsAuditLogSettings(
	ctx context.Context,
	req *openapingv1beta2.ListOpsAuditLogSettingsRequest,
) (*openapingv1beta2.ListOpsAuditLogSettingsResponse, error) {
	ctx, span := tracer.Start(ctx, "ListRegions")
	defer span.End()

	// Extract request meta.
	reqMeta, err := metadata.ExtractRequestMetaWithCheckPermission(ctx)
	if err != nil {
		return nil, bizerr.WrapErrorWithCode(err, bizerr.ErrCodeParams, "failed to extract request metadata")
	}

	pName, err := enum.FromString[openapicommonv1beta1.Region_CloudProvider](req.GetCloudProvider())
	if err != nil {
		return nil, bizerr.WrapErrorWithCode(err, bizerr.ErrCodeParams, "invalid cloud provider")
	}

	orgId := reqMeta.OrgID
	resp, err := t.nextgenCli.ListRegions(ctx, &globalngv1.ListRegionsRequest{
		OrganizationId:  orgId,
		ServicePlan:     common.CvtGlobalServicePlan(req.GetServicePlan()),
		CloudProviderId: globalcommonv1.CloudProvider_Name(pName),
		State:           globalcommonv1.Region_ACTIVE,
	})
	if err != nil {
		return nil, bizerror.NewInternal(err, "failed to list regions")
	}

	opsAuditLogSettings := make([]*openapingv1beta2.OpsAuditLogSetting, 0, len(resp.Regions))

	for _, region := range resp.Regions {
		setting, err := cvtRegionToOpsAuditlogSetting(region)
		if err != nil {
			return nil, bizerror.NewInternal(err, "failed to convert region to ops audit log setting")
		}
		opsAuditLogSettings = append(opsAuditLogSettings, setting)
	}

	thirdProviders, err := t.accountCli.GetTenantThirdPartyAccountProviders(ctx, orgId)
	if err != nil {
		return nil, bizerr.WrapGrpcError(err, "failed to get third party account providers")
	}
	if len(thirdProviders) > 0 {
		opsAuditLogSettings = lo.Filter(opsAuditLogSettings, func(r *openapingv1beta2.OpsAuditLogSetting, _ int) bool {
			return lo.Contains(thirdProviders, r.GetRegion().GetCloudProvider())
		})
	}

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

	if len(req.Status) > 0 {
		opsAuditLogSettings = lo.Filter(opsAuditLogSettings, func(r *openapingv1beta2.OpsAuditLogSetting, _ int) bool {
			return lo.Contains(req.Status, r.GetEnabled())
		})
	}
	if req.RegionId != "" {
		opsAuditLogSettings = lo.Filter(opsAuditLogSettings, func(r *openapingv1beta2.OpsAuditLogSetting, _ int) bool {
			return r.GetRegion().RegionId == req.RegionId
		})
	}

	// pagination
	total := int32(len(opsAuditLogSettings))
	pagedOpsAuditLogSettings, nextToken, err := common.LocalPagination(opsAuditLogSettings, req, ListRegionsPageSize, common.ListMaxPageSize)
	if err != nil {
		return nil, bizerror.NewInternal(err, "failed to paginate regions")
	}

	re := &openapingv1beta2.ListOpsAuditLogSettingsResponse{
		TotalSize:           total,
		NextPageToken:       nextToken,
		OpsAuditLogSettings: pagedOpsAuditLogSettings,
	}

	return re, nil
}

func cvtRegionToOpsAuditlogSetting(region *globalcommonv1.Region) (s *openapingv1beta2.OpsAuditLogSetting, err error) {
	openapiName, openapiId, cloudProvider, err := common.BuildOpenApiRegionName(
		region.CloudProviderId.String(),
		region.Id.String(),
	)
	if err != nil {
		return nil, bizerror.NewInternal(err, "failed to build openapi region name")
	}

	openApiRegion := &openapicommonv1beta1.Region{
		Name:          openapiName,
		RegionId:      openapiId,
		CloudProvider: cloudProvider,
		DisplayName:   region.GetDisplayName(),
		ServicePlans:  cvtRegionServicePlans(region),
	}

	setting := &openapingv1beta2.OpsAuditLogSetting{
		Region: openApiRegion,
	}

	regionSetting := region.GetOpsAuditLogSetting()
	if regionSetting != nil {
		setting.Enabled = regionSetting.Enabled
		if location := regionSetting.GetLogLocation(); location != nil {
			setting.LogLocation = &openapingv1beta2.OpsAuditLogSetting_LogLocation{
				BucketUri:  location.GetBucketUri(),
				RoleArn:    location.GetRoleArn(),
				AccountId:  location.GetAccountId(),
				ExternalId: location.GetExternalId(),
			}
		}
		if rotation := regionSetting.GetLogRotation(); rotation != nil {
			setting.LogRotation = &openapingv1beta2.OpsAuditLogSetting_LogRotation{
				FileSizeLimitMb:     int64(rotation.GetFileSizeLimitInMb()),
				RotationIntervalMin: int64(rotation.GetRotationIntervalInMin()),
			}
		}
	}

	return setting, nil
}

func cvtRegionServicePlans(region *globalcommonv1.Region) []openapicommonv1beta1.ServicePlan {
	servicePlans := lo.Map(region.GetRegionWhiteLists(), func(wl *globalcommonv1.RegionWhiteList, _ int) openapicommonv1beta1.ServicePlan {
		return common.CvtOpenAPIServicePlan(wl.GetServicePlan())
	})
	servicePlans = lo.Filter(servicePlans, func(plan openapicommonv1beta1.ServicePlan, _ int) bool {
		return plan != openapicommonv1beta1.ServicePlan_SERVICE_PLAN_UNSPECIFIED
	})
	if len(servicePlans) == 0 && region.GetServicePlan() != globalcommonv1.ServicePlan_SERVICE_PLAN_UNSPECIFIED {
		servicePlans = append(servicePlans, common.CvtOpenAPIServicePlan(region.GetServicePlan()))
	}
	return lo.Uniq(servicePlans)
}
