package region

import (
	"hash/fnv"
	"strings"

	"github.com/google/uuid"
	"github.com/samber/lo"

	globalcommonv1 "github.com/tidbcloud/tidb-management-service/api/spec/global/common/v1"
	"github.com/tidbcloud/tidb-management-service/dedicated-global-service/domain/entity"
	"github.com/tidbcloud/tidb-management-service/dedicated-global-service/infra/repoimpl/schema"
	"github.com/tidbcloud/tidb-management-service/utilities/bizerr"
	"github.com/tidbcloud/tidb-management-service/utilities/enum"
)

func CloudRegionModelToEn(region *schema.Region, provider *schema.Provider) (*entity.Region, bizerr.BizError) {
	if region == nil {
		return nil, bizerr.New(bizerr.ErrCodeUnknown, "region is nil")
	}

	out := &entity.Region{
		Id:                0, // fill later
		LogicalName:       region.Name,
		CloudProviderId:   0, // fill later
		DisplayName:       region.DisplayName,
		OpenedTenantIds:   nil, // fill later
		SortPriority:      region.Priority,
		Area:              region.Area,
		CountryCode:       region.CountryCode,
		State:             0,     // fill later
		IsPublic:          false, // fill later
		DefaultBackupTime: region.DefaultBackupTime,
		ServiceAddress:    region.ServiceAddress,
	}

	// fill CloudProviderId
	cloudProviderName, err := enum.FromString[globalcommonv1.CloudProvider_Name](provider.Name)
	if err != nil {
		return nil, bizerr.New(bizerr.ErrCodeUnknown, "cloud provider %s is not supported", provider.Name)
	}
	out.CloudProviderId = globalcommonv1.CloudProvider_Name(cloudProviderName)

	// fill Id
	regionName, err := enum.FromString[globalcommonv1.Region_Name](region.RegionName)
	if err != nil {
		return nil, bizerr.New(bizerr.ErrCodeUnknown, "region %s is not supported", region.Name)
	}
	out.Id = globalcommonv1.Region_Name(regionName)

	// fill State
	state, err := enum.FromString[globalcommonv1.Region_State](region.Status)
	if err != nil {
		return nil, bizerr.New(bizerr.ErrCodeUnknown, "unknown region state: %s", region.Status)
	}
	out.State = globalcommonv1.Region_State(state)

	// fill IsPublic
	if region.IsPublic != nil {
		out.IsPublic = *region.IsPublic
	}

	// fill OpenedTenantIds
	if !out.IsPublic {
		openedTenantIds := make([]uint64, 0, len(region.RegionTenants))
		for _, t := range region.RegionTenants {
			openedTenantIds = append(openedTenantIds, t.TenantId)
		}
		out.OpenedTenantIds = openedTenantIds
	}

	return out, nil
}

func CloudRegionEnToModel(region *entity.Region) (*schema.Region, bizerr.BizError) {
	if region == nil {
		return nil, bizerr.New(bizerr.ErrCodeUnknown, "region is nil")
	}

	out := &schema.Region{
		Name:              region.LogicalName,
		RegionName:        region.Id.String(),
		DisplayName:       region.DisplayName,
		Priority:          region.SortPriority,
		Area:              region.Area,
		CountryCode:       region.CountryCode,
		Status:            region.State.String(),
		IsPublic:          lo.ToPtr(region.IsPublic),
		DefaultBackupTime: region.DefaultBackupTime,
		ServiceAddress:    region.ServiceAddress,
		RegionTenants:     nil, // not fill
	}

	return out, nil
}

func CloudProviderModelToEn(provider *schema.Provider, orgId uint64) (*entity.CloudProvider, bizerr.BizError) {
	if provider == nil {
		return nil, bizerr.New(bizerr.ErrCodeUnknown, "provider is nil")
	}

	out := &entity.CloudProvider{
		Id:          0, // fill later
		DisplayName: provider.DisplayName,
		State:       0, // fill later
		Regions:     make([]*globalcommonv1.Region, 0, len(provider.Regions)),
	}

	// fill State
	state, rawErr := enum.FromString[globalcommonv1.CloudProvider_State](provider.Status)
	if rawErr != nil {
		return nil, bizerr.New(bizerr.ErrCodeUnknown, "unknown cloud provider state: %s", provider.Status)
	}
	out.State = globalcommonv1.CloudProvider_State(state)

	// fill Id
	cloudProviderId, rawErr := enum.FromString[globalcommonv1.CloudProvider_Name](provider.Name)
	if rawErr != nil {
		return nil, bizerr.New(bizerr.ErrCodeUnknown, "cloud provider %s is not supported", provider.Name)
	}
	out.Id = globalcommonv1.CloudProvider_Name(cloudProviderId)

	// fill Regions
	for _, each := range provider.Regions {
		region, err := CloudRegionModelToEn(each, provider)
		if err != nil {
			return nil, bizerr.New(bizerr.ErrCodeUnknown, "%s", err.Error())
		}
		out.Regions = append(out.Regions, region)
	}

	out.Regions = filterRegionsByOrgId(out.Regions, orgId)

	return out, nil
}

func CloudProviderEnToModel(provider *entity.CloudProvider) (*schema.Provider, bizerr.BizError) {
	if provider == nil {
		return nil, bizerr.New(bizerr.ErrCodeUnknown, "provider is nil")
	}

	out := &schema.Provider{
		Name:        provider.Id.String(),
		DisplayName: provider.DisplayName,
		Status:      provider.State.String(),
		Regions:     nil, // not fill
	}

	return out, nil
}

func buildCloudProviders(providers []*schema.Provider, orgId uint64) ([]*entity.CloudProvider, bizerr.BizError) {
	if len(providers) == 0 {
		return nil, bizerr.New(bizerr.ErrCodeUnknown, "providers is empty")
	}

	out := make([]*entity.CloudProvider, 0, len(providers))
	for _, each := range providers {
		provider, err := CloudProviderModelToEn(each, orgId)
		if err != nil {
			return nil, bizerr.New(bizerr.ErrCodeUnknown, "%s", err.Error())
		}
		out = append(out, provider)
	}
	return out, nil
}

func GenRegionModelId() uint64 {
	const (
		regionIdStartAt = 20000000
		regionIdMod     = 1000000
	)

	h := fnv.New64()
	uuidStr := strings.Replace(uuid.New().String(), "-", "", 4)
	_, _ = h.Write([]byte(uuidStr))

	return h.Sum64()%regionIdMod + regionIdStartAt
}

// if orgId is 0, return all regions
// if orgId is not 0, return public regions and private regions that opened to the org
// if orgId is not 0 and there are private logical regions, only return private logical regions
func filterRegionsByOrgId(regions []*entity.Region, orgId uint64) []*entity.Region {
	if orgId == 0 {
		return regions
	}

	// filter regions by org id
	var (
		filteredRegions       []*entity.Region
		privateLogicalRegions []*entity.Region
		hasPrivateRegion      = false
	)

	for _, r := range regions {
		if r.IsPublic {
			filteredRegions = append(filteredRegions, r)
			continue
		}

		// private region
		_, open := lo.Find(r.OpenedTenantIds, func(openOrgId uint64) bool {
			return openOrgId == orgId
		})
		if open {
			if r.GetLogicalName() != r.Id.String() {
				// private logical region
				hasPrivateRegion = true
				privateLogicalRegions = append(privateLogicalRegions, r)
			} else {
				filteredRegions = append(filteredRegions, r)
			}
			continue
		}
	}

	if hasPrivateRegion {
		return privateLogicalRegions
	}

	return filteredRegions
}
