package grpcli

import (
	"context"
	"fmt"
	"strings"

	"github.com/tidbcloud/dataflow-service/pkg/log"
	"github.com/tidbcloud/pkgv2/bizerr"
	datatypev1 "github.com/tidbcloud/tidb-management-service/api/spec/datatype/v1"
	global_commonv1 "github.com/tidbcloud/tidb-management-service/api/spec/global/common/v1"
	tidb_mgmt_servicev1 "github.com/tidbcloud/tidb-management-service/api/spec/tidb_mgmt_service/v1"
	"go.uber.org/zap"
	"google.golang.org/protobuf/proto"
)

type RegionGrpcClient interface {
	GetRegionByTenantID(
		ctx context.Context, tenantID uint64, cloudProvider, regionName string,
	) (*tidb_mgmt_servicev1.Region, bizerr.BizError)
	ListActiveRegions(ctx context.Context) ([]*tidb_mgmt_servicev1.Region, bizerr.BizError)
}

type RegionGrpcClientWithLookupOptions interface {
	GetRegionByTenantIDWithOptions(
		ctx context.Context,
		tenantID uint64,
		cloudProvider, regionName string,
		options RegionLookupOptions,
	) (*tidb_mgmt_servicev1.Region, bizerr.BizError)
}

type RegionLookupOptions struct {
	DeployType  tidb_mgmt_servicev1.DeployType
	ServicePlan global_commonv1.ServicePlan
}

type regionGrpcClientImpl struct {
	client tidb_mgmt_servicev1.RegionServiceClient
}

func NewRegionGrpcClient(ctx context.Context, addr string) RegionGrpcClient {
	logger := log.GetLogger(ctx)
	logger.Info("initializing region service client", zap.String("addr", addr))

	conn := MustBuildGrpcClientConnection(ctx, addr)
	client := tidb_mgmt_servicev1.NewRegionServiceClient(conn)
	return &regionGrpcClientImpl{
		client: client,
	}
}

// GetRegionByTenantID get the logical region by tenant id.
// With logical region, there may be multiple logical regions in a physical region.
// But we ensure there is only one logical region for a tenant in a physical region,
// so tenant id and physical region name can determine one logical region.
func (m *regionGrpcClientImpl) GetRegionByTenantID(
	ctx context.Context, tenantID uint64, cloudProvider, regionName string,
) (*tidb_mgmt_servicev1.Region, bizerr.BizError) {
	return m.GetRegionByTenantIDWithOptions(ctx, tenantID, cloudProvider, regionName, RegionLookupOptions{})
}

func (m *regionGrpcClientImpl) GetRegionByTenantIDWithOptions(
	ctx context.Context,
	tenantID uint64,
	cloudProvider, regionName string,
	options RegionLookupOptions,
) (*tidb_mgmt_servicev1.Region, bizerr.BizError) {
	logger := log.GetLogger(ctx)
	regionID := global_commonv1.Region_NAME_UNSPECIFIED

	if cloudProvider != "" {
		cloudRegionID, err := RegionFromString(cloudProvider, regionName)
		if err != nil {
			logger.Error("invalid cloud provider/region name", zap.String("cloudProvider", cloudProvider), zap.String("regionName", regionName), zap.Error(err))
			return nil, bizerr.New(bizerr.ErrCodeParams, "invalid cloud provider/region name %s/%s", cloudProvider, regionName)
		}
		regionID = cloudRegionID
	} else {
		regionID = RegionName2ID(regionName)
	}

	if regionID == global_commonv1.Region_NAME_UNSPECIFIED {
		return nil, bizerr.New(bizerr.ErrCodeParams, "invalid cloud provider/region name %s/%s", cloudProvider, regionName)
	}

	deployType := options.DeployType
	if deployType == tidb_mgmt_servicev1.DeployType_DEPLOY_TYPE_UNSPECIFIED {
		deployType = tidb_mgmt_servicev1.DeployType_DEPLOY_TYPE_DEDICATED
	}

	req := &tidb_mgmt_servicev1.SelectRegionByOrganizationIdRequest{
		DeployType:     deployType,
		OrganizationId: tenantID,
		RegionId:       regionID,
		ServicePlan:    options.ServicePlan,
	}
	if deployType == tidb_mgmt_servicev1.DeployType_DEPLOY_TYPE_NEXTGEN {
		cloudProviderID := CloudProviderFromString(cloudProvider)
		if cloudProviderID == global_commonv1.CloudProvider_NAME_UNSPECIFIED {
			return nil, bizerr.New(bizerr.ErrCodeParams, "invalid cloud provider name %s", cloudProvider)
		}
		req.CloudProviderId = cloudProviderID
	}
	region, err := m.client.SelectRegionByOrganizationId(ctx, req)
	if err != nil {
		return nil, globalServiceError2BizError(err)
	}
	return region, nil
}

func RegionFromString(cloudProvider, regionName string) (global_commonv1.Region_Name, error) {
	if cloudProvider == "" || regionName == "" {
		return global_commonv1.Region_NAME_UNSPECIFIED, nil
	}

	enumProvider := normalizeRegionEnumProvider(cloudProvider)
	enumNameStr := strings.ReplaceAll(strings.ToUpper(fmt.Sprintf("%s_%s", enumProvider, regionName)), "-", "_")
	enumValue, ok := global_commonv1.Region_Name_value[enumNameStr]

	if !ok {
		return global_commonv1.Region_NAME_UNSPECIFIED, fmt.Errorf("invalid region enum value: %s", enumNameStr)
	}

	return global_commonv1.Region_Name(enumValue), nil
}

func normalizeRegionEnumProvider(cloudProvider string) string {
	normalizedProvider := strings.ToLower(strings.TrimSpace(cloudProvider))
	switch normalizedProvider {
	case "ack", "aliyun", "alicloud":
		return "alicloud"
	default:
		return normalizedProvider
	}
}

func CloudProviderFromString(cloudProvider string) global_commonv1.CloudProvider_Name {
	switch strings.ToLower(strings.TrimSpace(cloudProvider)) {
	case "aws", "eks":
		return global_commonv1.CloudProvider_AWS
	case "gcp", "gke":
		return global_commonv1.CloudProvider_GCP
	case "azure", "aks":
		return global_commonv1.CloudProvider_AZURE
	case "ack", "aliyun", "alicloud":
		return global_commonv1.CloudProvider_ALICLOUD
	default:
		return global_commonv1.CloudProvider_NAME_UNSPECIFIED
	}
}

func (m *regionGrpcClientImpl) ListActiveRegions(ctx context.Context) ([]*tidb_mgmt_servicev1.Region, bizerr.BizError) {

	req := &tidb_mgmt_servicev1.ListRegionsRequest{
		DeployType: tidb_mgmt_servicev1.DeployType_DEPLOY_TYPE_DEDICATED,
	}
	resp, err := m.client.ListRegions(ctx, req)
	if err != nil {
		return nil, bizerr.New(bizerr.ErrCodeUnknown, "can't list regions %v", err)
	}
	regions := []*tidb_mgmt_servicev1.Region{}
	for _, region := range resp.Regions {
		if region.Region.GetState() == global_commonv1.Region_ACTIVE {
			regions = append(regions, region)
		}
	}
	return regions, nil
}

// RegionName2ID get enum value by corresponding "datatype.v1.name" tag
// it is mainly copied from https://github.com/tidbcloud/tidb-management-service/blob/main/utilities/enum/strconv.go#L42
func RegionName2ID(regionName string) global_commonv1.Region_Name {
	if regionName == "" {
		return global_commonv1.Region_NAME_UNSPECIFIED
	}

	var r global_commonv1.Region_Name
	d := r.Descriptor()

	// Iterate over all enum values.
	for i := 0; i < d.Values().Len(); i++ {
		valueDesc := d.Values().Get(i)
		var name string
		if proto.HasExtension(valueDesc.Options(), datatypev1.E_Name) {
			ext := proto.GetExtension(valueDesc.Options(), datatypev1.E_Name)
			if extString, ok := ext.(string); ok && extString != "" {
				name = extString
			}
		}

		if name != "" && strings.EqualFold(name, regionName) {
			return global_commonv1.Region_Name(valueDesc.Number())
		}
	}

	return global_commonv1.Region_NAME_UNSPECIFIED
}
