package region

import (
	"hash/fnv"
	"strings"

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

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

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

	servicePlan := globalcommonv1.ServicePlan(globalcommonv1.ServicePlan_value[region.ServicePlan])
	servicePlans := convertRegionServicePlansModelToEnums(region.RegionServicePlans)
	out := &entity.Region{
		Region: &globalcommonv1.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,
			IsPublic:          false,
			DefaultBackupTime: region.DefaultBackupTime,
			ServiceAddress:    region.ServiceAddress,
			ServicePlan:       servicePlan,
			SharedBucketUrl:   lo.ToPtr(region.SharedBucketUrl),
		},
		RegionID:          uint64(region.ID),
		ProviderID:        region.ProviderId,
		RegionLogicalName: region.Name,
		RegionName:        region.RegionName,
	}

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

	// fill Id
	regionId, err := enum.RegionFromString(region.Provider.Name, region.RegionName)
	if err != nil || regionId == globalcommonv1.Region_NAME_UNSPECIFIED {
		return nil, bizerr.WrapErrorWithCode(err, bizerr.ErrCodeParams, "invalid region: %s", region.RegionName)
	}

	out.Id = regionId

	// 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 RegionWhiteLists from region_service_plans + region_tenants.
	out.RegionWhiteLists = buildRegionWhiteLists(region, servicePlans)

	// fill legacy fields from selected service plan whitelist when possible,
	// without fallback to deprecated region-level visibility fields.
	legacyWL := pickLegacyRegionWhiteList(out.RegionWhiteLists, out.GetServicePlan())
	if legacyWL != nil {
		out.IsPublic = legacyWL.GetIsPublic()
		out.OpenedTenantIds = lo.Uniq(append([]uint64(nil), legacyWL.GetOpenedTenantIds()...))
	}

	// fill ops audit log setting
	if region.RegionOpsAuditLogSetting != nil {
		opsAuditLogSettingEntity, err := convertRegionOpsAuditLogSettingModelToEntity(region.RegionOpsAuditLogSetting)
		if err != nil {
			return nil, bizerr.WrapErrorWithCode(err, bizerr.ErrCodeUnknown, "failed to convert region ops audit log setting model to entity")
		}
		out.OpsAuditLogSetting = opsAuditLogSettingEntity.RegionOpsAuditLogSetting
	}

	return out, nil
}

func buildRegionWhiteLists(
	region *schema.Region,
	servicePlans []globalcommonv1.ServicePlan,
) []*globalcommonv1.RegionWhiteList {
	if len(servicePlans) == 0 {
		return nil
	}

	servicePlanModelMap := make(map[globalcommonv1.ServicePlan]*schema.RegionServicePlan, len(region.RegionServicePlans))
	for _, servicePlanModel := range region.RegionServicePlans {
		plan := globalcommonv1.ServicePlan(globalcommonv1.ServicePlan_value[servicePlanModel.ServicePlan])
		if plan == globalcommonv1.ServicePlan_SERVICE_PLAN_UNSPECIFIED {
			continue
		}
		servicePlanModelMap[plan] = servicePlanModel
	}

	out := make([]*globalcommonv1.RegionWhiteList, 0, len(servicePlans))
	for _, plan := range servicePlans {
		servicePlanModel := servicePlanModelMap[plan]
		if servicePlanModel == nil {
			continue
		}

		isPublic := servicePlanModel.IsPublic == nil || *servicePlanModel.IsPublic

		openedTenantIds := make([]uint64, 0)
		if !isPublic {
			for _, tenant := range region.RegionTenants {
				if tenant.RegionServicePlanId != uint64(servicePlanModel.ID) {
					continue
				}
				openedTenantIds = append(openedTenantIds, tenant.TenantId)
			}
		}

		out = append(out, &globalcommonv1.RegionWhiteList{
			ServicePlan:     plan,
			IsPublic:        isPublic,
			OpenedTenantIds: lo.Uniq(openedTenantIds),
		})
	}

	return out
}

func pickLegacyRegionWhiteList(
	regionWhiteLists []*globalcommonv1.RegionWhiteList,
	servicePlan globalcommonv1.ServicePlan,
) *globalcommonv1.RegionWhiteList {
	if len(regionWhiteLists) == 0 {
		return nil
	}
	for _, wl := range regionWhiteLists {
		if wl.GetServicePlan() == servicePlan {
			return wl
		}
	}
	return regionWhiteLists[0]
}

func convertRegionServicePlansModelToEnums(
	servicePlans []*schema.RegionServicePlan,
) []globalcommonv1.ServicePlan {
	out := make([]globalcommonv1.ServicePlan, 0, len(servicePlans))
	for _, servicePlan := range servicePlans {
		plan := globalcommonv1.ServicePlan(globalcommonv1.ServicePlan_value[servicePlan.ServicePlan])
		if plan != globalcommonv1.ServicePlan_SERVICE_PLAN_UNSPECIFIED {
			out = append(out, plan)
		}
	}
	return lo.Uniq(out)
}

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

func convertCloudRegionEntityToModel(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
		ServicePlan:       region.ServicePlan.String(),
		SharedBucketUrl:   lo.FromPtr(region.SharedBucketUrl), // string value
	}

	return out, nil
}

func convertCloudProviderModelToEntity(provider *schema.Provider) (*entity.CloudProvider, bizerr.BizError) {
	// 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)
	}
	// fill State
	state, err := enum.FromString[globalcommonv1.CloudProvider_State](provider.Status)
	if err != nil {
		return nil, bizerr.New(bizerr.ErrCodeUnknown, "unknown cloud provider state: %s", provider.Status)
	}
	return &entity.CloudProvider{
		ProviderID: uint64(provider.ID),
		CloudProvider: &globalcommonv1.CloudProvider{
			Id:           globalcommonv1.CloudProvider_Name(cloudProviderName),
			DisplayName:  provider.DisplayName,
			State:        globalcommonv1.CloudProvider_State(state),
			IsPublic:     provider.IsPublic,
			ServicePlan:  globalcommonv1.ServicePlan(globalcommonv1.ServicePlan_value[provider.ServicePlan]),
			ServicePlans: nil,
		},
	}, nil
}

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

	out := &schema.Provider{
		Name:        cloudProvider.Id.String(),
		DisplayName: cloudProvider.DisplayName,
		Status:      cloudProvider.State.String(),
		IsPublic:    cloudProvider.IsPublic,
		ServicePlan: cloudProvider.ServicePlan.String(),
	}
	if out.ServicePlan == globalcommonv1.ServicePlan_SERVICE_PLAN_UNSPECIFIED.String() {
		out.ServicePlan = globalcommonv1.ServicePlan_Premium.String()
	}
	if cloudProvider.ProviderID != 0 {
		out.ID = uint(cloudProvider.ProviderID)
	}
	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
}

func convertRegionOpsAuditLogSettingEntityToModel(en *entity.RegionOpsAuditLogSetting) (*schema.RegionOpsAuditLogSetting, bizerr.BizError) {
	if en == nil {
		return nil, bizerr.New(bizerr.ErrCodeUnknown, "region ops audit log setting is nil")
	}
	if en.RegionID == 0 {
		return nil, bizerr.New(bizerr.ErrCodeParams, "regionId is required")
	}

	schemaSetting := &schema.RegionOpsAuditLogSetting{
		Enabled:  lo.ToPtr(en.Enabled),
		RegionId: en.RegionID,
	}

	if en.LogLocation != nil {
		schemaSetting.Type = en.GetLogLocation().Type.String()
		schemaSetting.BucketUri = en.GetLogLocation().BucketUri
		schemaSetting.RoleArn = en.GetLogLocation().RoleArn
		schemaSetting.ExternalId = en.GetLogLocation().ExternalId
		schemaSetting.AccountId = en.GetLogLocation().AccountId
	}
	if en.LogRotation != nil {
		schemaSetting.FileSizeLimitInMb = en.GetLogRotation().FileSizeLimitInMb
		schemaSetting.RotationIntervalInMin = en.GetLogRotation().RotationIntervalInMin
	}

	return schemaSetting, nil
}

func convertRegionOpsAuditLogSettingModelToEntity(mo *schema.RegionOpsAuditLogSetting) (*entity.RegionOpsAuditLogSetting, bizerr.BizError) {
	if mo == nil {
		return nil, bizerr.New(bizerr.ErrCodeUnknown, "region ops audit log setting is nil")
	}

	return &entity.RegionOpsAuditLogSetting{
		RegionOpsAuditLogSetting: &globalcommonv1.RegionOpsAuditLogSetting{
			Enabled: mo.Enabled != nil && *mo.Enabled,
			LogLocation: &globalcommonv1.RegionOpsAuditLogSetting_LogLocation{
				Type:       globalcommonv1.RegionOpsAuditLogSetting_Type(globalcommonv1.RegionOpsAuditLogSetting_Type_value[mo.Type]),
				BucketUri:  mo.BucketUri,
				RoleArn:    mo.RoleArn,
				ExternalId: mo.ExternalId,
				AccountId:  mo.AccountId,
			},
			LogRotation: &globalcommonv1.RegionOpsAuditLogSetting_LogRotation{
				FileSizeLimitInMb:     mo.FileSizeLimitInMb,
				RotationIntervalInMin: mo.RotationIntervalInMin,
			},
		},
		RegionID: mo.RegionId,
	}, nil
}

func ConvertProviderIdentityModelToEntity(mo *schema.ProviderIdentity) (*entity.RegionProviderIdentity, bizerr.BizError) {
	if mo == nil {
		return nil, bizerr.New(bizerr.ErrCodeUnknown, "provider identity is nil")
	}

	en := &entity.RegionProviderIdentity{
		RegionProviderIdentity: &globalngv1.RegionProviderIdentity{
			Id:       mo.ID,
			Identity: mo.Identity,
		},
		RegionId: mo.RegionId,
	}

	return en, nil
}

func convertProviderIdentityEntityToModel(en *entity.RegionProviderIdentity) (*schema.ProviderIdentity, bizerr.BizError) {
	if en == nil {
		return nil, bizerr.New(bizerr.ErrCodeUnknown, "provider identity is nil")
	}

	mo := &schema.ProviderIdentity{
		ID:       en.Id,
		RegionId: en.RegionId,
		Identity: en.Identity,
	}

	return mo, nil
}

func convertDefaultProviderIdentityModelToEntity(mo *schema.DefaultProviderIdentity) *entity.DefaultProviderIdentity {
	if mo == nil {
		return nil
	}

	return &entity.DefaultProviderIdentity{
		ID:          mo.ID,
		CreatedAt:   mo.CreatedAt,
		UpdatedAt:   mo.UpdatedAt,
		ProviderId:  mo.ProviderId,
		ServicePlan: mo.ServicePlan,
		RegionId:    mo.RegionId,
		TenantId:    mo.TenantId,
		Identity:    mo.Identity,
	}
}

func convertDefaultProviderIdentityEntityToModel(
	en *entity.DefaultProviderIdentity) *schema.DefaultProviderIdentity {
	if en == nil {
		return nil
	}

	return &schema.DefaultProviderIdentity{
		Model: gorm.Model{
			ID: en.ID,
		},
		ProviderId:  en.ProviderId,
		ServicePlan: en.ServicePlan,
		RegionId:    en.RegionId,
		TenantId:    en.TenantId,
		Identity:    en.Identity,
	}
}
