package region

import (
	"cmp"
	"context"
	"fmt"
	"slices"
	"strconv"

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

	globalcommonv1 "github.com/tidbcloud/tidb-management-service/api/spec/global/common/v1"
	openapicommonv1beta1 "github.com/tidbcloud/tidb-management-service/api/spec/tidb_cloud_open_api/common/v1beta1"
	openapidedicatedv1beta1 "github.com/tidbcloud/tidb-management-service/api/spec/tidb_cloud_open_api/dedicated/v1beta1"
	"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 *openapidedicatedv1beta1.ListRegionsRequest,
) (*openapidedicatedv1beta1.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")
	}

	projectId := reqMeta.DefaultProjectID
	if req.GetProjectId() != "" {
		projectId, err = strconv.ParseUint(req.GetProjectId(), 10, 64)
		if err != nil {
			return nil, bizerr.WrapErrorWithCode(err, bizerr.ErrCodeParams, "invalid project id")
		}
	}

	regions, err := t.listRegions(ctx, reqMeta.OrgID, projectId, req.GetCloudProvider())
	if err != nil {
		return nil, err
	}

	// 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 &openapidedicatedv1beta1.ListRegionsResponse{
		Regions:       pagedRegions,
		TotalSize:     total,
		NextPageToken: nextToken,
	}, nil
}

func (t serverImpl) GetRegion(
	ctx context.Context,
	req *openapidedicatedv1beta1.GetRegionRequest,
) (*openapicommonv1beta1.Region, error) {
	ctx, span := tracer.Start(ctx, "GetRegion")
	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")
	}

	projectId := reqMeta.DefaultProjectID
	if req.GetProjectId() != "" {
		projectId, err = strconv.ParseUint(req.GetProjectId(), 10, 64)
		if err != nil {
			return nil, bizerr.WrapErrorWithCode(err, bizerr.ErrCodeParams, "invalid project id")
		}
	}

	regions, err := t.listRegions(ctx, reqMeta.OrgID, projectId, "")
	if err != nil {
		return nil, err
	}

	r := lo.Filter(regions, func(r *openapicommonv1beta1.Region, _ int) bool {
		return r.GetRegionId() == req.GetRegionId()
	})
	if len(r) == 0 {
		return nil, bizerror.NewNotFound(fmt.Errorf("region not found"), "region not found")
	}

	return r[0], nil
}

// orgId and projectId are required.
// cloudProviderStr is optional, if it's empty, all regions will be returned.
func (t serverImpl) listRegions(
	ctx context.Context,
	orgId, projectId uint64,
	cloudProviderStr string,
) ([]*openapicommonv1beta1.Region, error) {
	ps, err := t.listProviders(ctx, orgId, projectId)
	if err != nil {
		return nil, bizerror.NewInternal(err, "failed to list cloud providers")
	}

	// no cloud provider available
	if len(ps) == 0 {
		return []*openapicommonv1beta1.Region{}, nil
	}

	regions := make([]*openapicommonv1beta1.Region, 0, len(ps)*3) // suppose each cloud provider has 3 regions.

	for _, p := range ps {
		if cloudProviderStr != "" {
			// Validate cloud provider query parameter.
			pName, err := enum.FromString[openapicommonv1beta1.Region_CloudProvider](cloudProviderStr)
			if err != nil {
				return nil, bizerr.WrapErrorWithCode(err, bizerr.ErrCodeParams, "invalid cloud provider")
			}
			if openapicommonv1beta1.Region_CloudProvider(pName) != common.GlobalProviderToOpenApi(p.GetId()) {
				continue
			}
		}

		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 cloud provider.
			if cloudProviderStr != "" && cloudProviderStr != cloudProvider.String() {
				continue
			}

			// 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(),
			})
		}
	}
	return regions, nil
}
