// Package grpc 将 user-service 认证、用户和短号用例适配成 protobuf gRPC 服务。
package grpc

import (
	userv1 "hyapp.local/api/proto/user/v1"
	authdomain "hyapp/services/user-service/internal/domain/auth"
	hostdomain "hyapp/services/user-service/internal/domain/host"
	invitedomain "hyapp/services/user-service/internal/domain/invite"
	mictimedomain "hyapp/services/user-service/internal/domain/mictime"
	userdomain "hyapp/services/user-service/internal/domain/user"
	authservice "hyapp/services/user-service/internal/service/auth"
	userservice "hyapp/services/user-service/internal/service/user"
)

// toProtoApp 把 App 注册表事实转换为 protobuf 投影。
func toProtoApp(app userdomain.App) *userv1.App {
	return &userv1.App{
		AppId:       app.AppID,
		AppCode:     app.AppCode,
		AppName:     app.AppName,
		PackageName: app.PackageName,
		Platform:    app.Platform,
		Status:      app.Status,
		CreatedAtMs: app.CreatedAtMs,
		UpdatedAtMs: app.UpdatedAtMs,
	}
}

// toProtoToken 把领域令牌转换为 protobuf 响应。
func toProtoToken(token authdomain.Token) *userv1.AuthToken {
	// Token 中包含 refresh token 原文，只能出现在登录/刷新响应路径。
	return &userv1.AuthToken{
		AppCode:                  token.AppCode,
		UserId:                   token.UserID,
		DisplayUserId:            token.DisplayUserID,
		DefaultDisplayUserId:     token.DefaultDisplayUserID,
		DisplayUserIdKind:        token.DisplayUserIDKind,
		DisplayUserIdExpiresAtMs: token.DisplayUserIDExpiresAtMs,
		ProfileCompleted:         token.ProfileCompleted,
		OnboardingStatus:         token.OnboardingStatus,
		SessionId:                token.SessionID,
		AccessToken:              token.AccessToken,
		RefreshToken:             token.RefreshToken,
		ExpiresInSec:             token.ExpiresInSec,
		TokenType:                token.TokenType,
	}
}

func toProtoRegisterRiskConfig(config authdomain.RegisterRiskConfig) *userv1.RegisterRiskConfig {
	return &userv1.RegisterRiskConfig{
		AppCode:              config.AppCode,
		MaxAccountsPerDevice: config.MaxAccountsPerDevice,
		UpdatedAtMs:          config.UpdatedAtMs,
		UpdatedByAdminId:     config.UpdatedByAdminID,
	}
}

// toProtoUser 把领域用户转换为 protobuf 用户投影。
func toProtoUser(user userdomain.User) *userv1.User {
	// User proto 只暴露用户主状态和当前展示短号，不包含密码、三方身份或房间状态。
	return &userv1.User{
		AppCode:                  user.AppCode,
		UserId:                   user.UserID,
		Status:                   toProtoStatus(user.Status),
		CreatedAtMs:              user.CreatedAtMs,
		UpdatedAtMs:              user.UpdatedAtMs,
		DisplayUserId:            user.CurrentDisplayUserID,
		DefaultDisplayUserId:     user.DefaultDisplayUserID,
		DisplayUserIdKind:        string(user.CurrentDisplayUserIDKind),
		DisplayUserIdExpiresAtMs: user.CurrentDisplayUserIDExpiresAtMs,
		Username:                 user.Username,
		Gender:                   user.Gender,
		Country:                  user.Country,
		Avatar:                   user.Avatar,
		ProfileBgImg:             user.ProfileBgImg,
		ContactInfo:              user.ContactInfo,
		WithdrawUsdtTrc20Address: user.WithdrawUSDTTRC20Address,
		Birth:                    user.BirthDate,
		Language:                 user.Language,
		CountryId:                user.CountryID,
		CountryName:              user.CountryName,
		CountryDisplayName:       user.CountryDisplayName,
		RegionId:                 user.RegionID,
		RegionCode:               user.RegionCode,
		RegionName:               user.RegionName,
		ProfileCompleted:         user.ProfileCompleted,
		ProfileCompletedAtMs:     user.ProfileCompletedAtMs,
		OnboardingStatus:         string(user.OnboardingStatus),
		IsoNumeric:               user.ISONumeric,
		PhoneCountryCode:         user.PhoneCountryCode,
		CountryEnabled:           user.CountryEnabled,
		Invite:                   toProtoInviteOverview(user.InviteOverview),
		PrettyId:                 user.PrettyID,
		PrettyDisplayUserId:      user.PrettyDisplayUserID,
		ContentBlacklisted:       user.ContentBlacklisted,
	}
}

func toProtoUserContentBlacklistEntry(item userdomain.ContentBlacklistEntry) *userv1.UserContentBlacklistEntry {
	return &userv1.UserContentBlacklistEntry{
		UserId:               item.UserID,
		DisplayUserId:        item.CurrentDisplayUserID,
		DefaultDisplayUserId: item.DefaultDisplayUserID,
		PrettyId:             item.PrettyID,
		PrettyDisplayUserId:  item.PrettyDisplayUserID,
		Username:             item.Username,
		Avatar:               item.Avatar,
		UserStatus:           toProtoStatus(item.UserStatus),
		OperatorAdminId:      item.OperatorAdminID,
		Reason:               item.Reason,
		CreatedAtMs:          item.CreatedAtMs,
		UpdatedAtMs:          item.UpdatedAtMs,
	}
}

func toProtoBusinessUserLookupItem(item userdomain.BusinessUserLookupItem) *userv1.BusinessUserLookupItem {
	return &userv1.BusinessUserLookupItem{
		UserId:        item.UserID,
		DisplayUserId: item.DisplayUserID,
		Username:      item.Username,
		Avatar:        item.Avatar,
		Status:        toProtoStatus(item.Status),
		RegionId:      item.RegionID,
		RegionCode:    item.RegionCode,
		RegionName:    item.RegionName,
	}
}

func toProtoManagerUserBlock(block userservice.ManagerUserBlock) *userv1.ManagerUserBlock {
	// 经理封禁记录是 H5 弹窗的列表数据；这里仅暴露可展示和可操作的稳定字段。
	return &userv1.ManagerUserBlock{
		BlockId:             block.BlockID,
		ManagerUserId:       block.ManagerUserID,
		TargetUserId:        block.TargetUserID,
		TargetDisplayUserId: block.TargetDisplayUserID,
		TargetUsername:      block.TargetUsername,
		TargetAvatar:        block.TargetAvatar,
		Country:             block.Country,
		BlockedUntilMs:      block.BlockedUntilMS,
		Status:              block.Status,
		Reason:              block.Reason,
		CreatedAtMs:         block.CreatedAtMS,
		UpdatedAtMs:         block.UpdatedAtMS,
	}
}

func toProtoAdminUserBan(ban userservice.AdminUserBan) *userv1.AdminUserBan {
	if ban.BanID == "" {
		return nil
	}
	return &userv1.AdminUserBan{
		BanId:             ban.BanID,
		CommandId:         ban.CommandID,
		TargetUserId:      ban.TargetUserID,
		ExpiresAtMs:       ban.ExpiresAtMs,
		Permanent:         ban.ExpiresAtMs == 0,
		Status:            ban.Status,
		Reason:            ban.Reason,
		OperatorAdminId:   ban.OperatorAdminID,
		CreatedAtMs:       ban.CreatedAtMs,
		UpdatedAtMs:       ban.UpdatedAtMs,
		ReleasedByAdminId: ban.ReleasedByAdminID,
		ReleasedReason:    ban.ReleasedReason,
		ReleasedAtMs:      ban.ReleasedAtMs,
	}
}

func toProtoUserAdminProfile(profile userdomain.AdminProfile) *userv1.UserAdminProfile {
	return &userv1.UserAdminProfile{
		User:                 toProtoUser(profile.User),
		Birth:                profile.BirthDate,
		RegisterDevice:       profile.RegisterDevice,
		LastSuccessLoginAtMs: profile.LastSuccessLoginAtMs,
		Roles:                append([]string(nil), profile.Roles...),
		Ban:                  toProtoActiveUserBanSummary(profile.Ban),
		LastOperatorType:     profile.LastOperatorType,
		LastOperatorUserId:   profile.LastOperatorUserID,
		LastOperationReason:  profile.LastOperationReason,
		LastOperationAtMs:    profile.LastOperationAtMs,
	}
}

func toProtoActiveUserBanSummary(summary userdomain.ActiveBanSummary) *userv1.ActiveUserBanSummary {
	if !summary.Active {
		return nil
	}
	return &userv1.ActiveUserBanSummary{
		Active:         true,
		Source:         summary.Source,
		BanId:          summary.BanID,
		Permanent:      summary.Permanent,
		ExpiresAtMs:    summary.ExpiresAtMs,
		UserStatus:     toProtoStatus(summary.UserStatus),
		OperatorType:   summary.OperatorType,
		OperatorUserId: summary.OperatorUserID,
		Reason:         summary.Reason,
		CreatedAtMs:    summary.CreatedAtMs,
		ActiveBanCount: summary.ActiveBanCount,
	}
}

func toProtoInviteOverview(overview invitedomain.Overview) *userv1.InviteOverview {
	if overview.MyInviteCode == "" && !overview.InviteEnabled && overview.InviteCount == 0 && overview.ValidInviteCount == 0 && overview.ValidInviteThresholdCoin == 0 {
		return nil
	}
	return &userv1.InviteOverview{
		MyInviteCode:             overview.MyInviteCode,
		InviteEnabled:            overview.InviteEnabled,
		InviteCount:              overview.InviteCount,
		ValidInviteCount:         overview.ValidInviteCount,
		ValidInviteThresholdCoin: overview.ValidInviteThresholdCoin,
	}
}

func toProtoInviteBinding(binding invitedomain.Binding) *userv1.InviteBinding {
	if !binding.Bound && binding.InviteCode == "" && binding.InviterUserID == 0 {
		return nil
	}
	return &userv1.InviteBinding{
		Bound:         binding.Bound,
		InviteCode:    binding.InviteCode,
		InviterUserId: binding.InviterUserID,
	}
}

func toProtoInviteAttribution(attribution invitedomain.Attribution) *userv1.InviteAttribution {
	return &userv1.InviteAttribution{
		Found:                         attribution.Found,
		AppCode:                       attribution.AppCode,
		InvitedUserId:                 attribution.InvitedUserID,
		InviterUserId:                 attribution.InviterUserID,
		InviteCode:                    attribution.InviteCode,
		BoundAtMs:                     attribution.BoundAtMS,
		ValidityStatus:                attribution.ValidityStatus,
		AccumulatedRechargeCoinAmount: attribution.AccumulatedRechargeCoinAmount,
		RequiredRechargeCoinAmount:    attribution.RequiredRechargeCoinAmount,
		ValidAtMs:                     attribution.ValidAtMS,
	}
}

func toProtoMicLifetimeStats(stats mictimedomain.LifetimeStats) *userv1.UserMicLifetimeStats {
	return &userv1.UserMicLifetimeStats{
		AppCode:        stats.AppCode,
		UserId:         stats.UserID,
		SeatOccupiedMs: stats.SeatOccupiedMs,
		MicOnlineMs:    stats.MicOnlineMs,
		SessionCount:   stats.SessionCount,
		FirstMicAtMs:   stats.FirstMicAtMs,
		LastMicAtMs:    stats.LastMicAtMs,
		UpdatedAtMs:    stats.UpdatedAtMs,
	}
}

func toProtoProfileStats(stats userdomain.ProfileStats) *userv1.UserProfileStats {
	return &userv1.UserProfileStats{
		AppCode:        stats.AppCode,
		UserId:         stats.UserID,
		VisitorsCount:  stats.VisitorsCount,
		FollowingCount: stats.FollowingCount,
		FollowersCount: stats.FollowersCount,
		FriendsCount:   stats.FriendsCount,
		UpdatedAtMs:    stats.UpdatedAtMs,
	}
}

func toProtoProfileVisitRecord(record userdomain.ProfileVisitRecord) *userv1.ProfileVisitRecord {
	return &userv1.ProfileVisitRecord{
		VisitorUserId:         record.VisitorUserID,
		TargetUserId:          record.TargetUserID,
		VisitCount:            record.VisitCount,
		LastVisitedAtMs:       record.LastVisitedAtMs,
		VisitorIdentityHidden: record.VisitorIdentityHidden,
		AnonymousVisitId:      record.AnonymousVisitID,
	}
}

func toProtoFollowRecord(record userdomain.FollowRecord) *userv1.FollowRecord {
	return &userv1.FollowRecord{
		FollowerUserId: record.FollowerUserID,
		FolloweeUserId: record.FolloweeUserID,
		FollowedAtMs:   record.FollowedAtMs,
	}
}

func toProtoFriendRecord(record userdomain.FriendRecord) *userv1.FriendRecord {
	return &userv1.FriendRecord{
		UserId:       record.UserID,
		FriendUserId: record.FriendUserID,
		FriendedAtMs: record.FriendedAtMs,
	}
}

func toProtoFriendApplication(application userdomain.FriendApplication) *userv1.FriendApplication {
	return &userv1.FriendApplication{
		RequesterUserId: application.RequesterUserID,
		TargetUserId:    application.TargetUserID,
		Status:          application.Status,
		CreatedAtMs:     application.CreatedAtMs,
		UpdatedAtMs:     application.UpdatedAtMs,
	}
}

func toProtoUserReport(report userdomain.Report) *userv1.UserReport {
	return &userv1.UserReport{
		ReportId:       report.ReportID,
		ReporterUserId: report.ReporterUserID,
		TargetType:     report.TargetType,
		UserId:         report.UserID,
		RoomId:         report.RoomID,
		ReportType:     report.ReportType,
		Reason:         report.Reason,
		ImageUrls:      append([]string{}, report.ImageURLs...),
		Status:         report.Status,
		CreatedAtMs:    report.CreatedAtMs,
	}
}

// toProtoCountry 把国家领域对象转换为管理 RPC 响应。
func toProtoCountry(country userdomain.Country) *userv1.Country {
	return &userv1.Country{
		AppCode:            country.AppCode,
		CountryId:          country.CountryID,
		CountryName:        country.CountryName,
		CountryCode:        country.CountryCode,
		CountryDisplayName: country.CountryDisplayName,
		SortOrder:          country.SortOrder,
		CreatedAtMs:        country.CreatedAtMs,
		UpdatedAtMs:        country.UpdatedAtMs,
		IsoAlpha3:          country.ISOAlpha3,
		IsoNumeric:         country.ISONumeric,
		PhoneCountryCode:   country.PhoneCountryCode,
		Enabled:            country.Enabled,
		Flag:               country.Flag,
	}
}

// toProtoRegion 把区域领域对象转换为管理 RPC 响应。
func toProtoRegion(region userdomain.Region) *userv1.Region {
	return &userv1.Region{
		AppCode:     region.AppCode,
		RegionId:    region.RegionID,
		RegionCode:  region.RegionCode,
		Name:        region.Name,
		Status:      region.Status,
		Countries:   append([]string{}, region.Countries...),
		SortOrder:   region.SortOrder,
		CreatedAtMs: region.CreatedAtMs,
		UpdatedAtMs: region.UpdatedAtMs,
	}
}

// toProtoIdentity 把短号领域投影转换为 protobuf 响应。
func toProtoIdentity(identity userdomain.Identity) *userv1.UserIdentity {
	// Identity proto 用于短号解析和修改结果，字段语义与领域 Identity 保持一致。
	// PrettyId/PrettyDisplayUserId 只表示当前 active 靓号，没有靓号时为空，不回填默认短号。
	return &userv1.UserIdentity{
		AppCode:                  identity.AppCode,
		UserId:                   identity.UserID,
		DisplayUserId:            identity.DisplayUserID,
		Status:                   string(identity.Status),
		DefaultDisplayUserId:     identity.DefaultDisplayUserID,
		DisplayUserIdKind:        string(identity.DisplayUserIDKind),
		DisplayUserIdExpiresAtMs: identity.DisplayUserIDExpiresAtMs,
		PrettyId:                 identity.PrettyID,
		PrettyDisplayUserId:      identity.PrettyDisplayUserID,
	}
}

func toProtoPrettyDisplayIDPool(pool userdomain.PrettyDisplayIDPool) *userv1.PrettyDisplayIDPool {
	// 池配置是后台和 App 可申请列表共用的展示信息，等级轨道和区间必须原样透出。
	return &userv1.PrettyDisplayIDPool{
		AppCode:          pool.AppCode,
		PoolId:           pool.PoolID,
		Name:             pool.Name,
		LevelTrack:       pool.LevelTrack,
		MinLevel:         pool.MinLevel,
		MaxLevel:         pool.MaxLevel,
		RuleType:         pool.RuleType,
		RuleConfigJson:   pool.RuleConfigJSON,
		Status:           pool.Status,
		SortOrder:        pool.SortOrder,
		CreatedByAdminId: pool.CreatedByAdminID,
		UpdatedByAdminId: pool.UpdatedByAdminID,
		CreatedAtMs:      pool.CreatedAtMs,
		UpdatedAtMs:      pool.UpdatedAtMs,
	}
}

func toProtoPrettyDisplayID(item userdomain.PrettyDisplayID) *userv1.PrettyDisplayID {
	// 后台发放号码没有 pool_id，Pool 会是零值；前端以 source 判断是否展示池信息。
	return &userv1.PrettyDisplayID{
		AppCode:          item.AppCode,
		PrettyId:         item.PrettyID,
		PoolId:           item.PoolID,
		Source:           item.Source,
		DisplayUserId:    item.DisplayUserID,
		Status:           item.Status,
		AssignedUserId:   item.AssignedUserID,
		AssignedLeaseId:  item.AssignedLeaseID,
		AssignedAtMs:     item.AssignedAtMs,
		ReleasedAtMs:     item.ReleasedAtMs,
		ReleaseReason:    item.ReleaseReason,
		GeneratedBatchId: item.GeneratedBatchID,
		CreatedByAdminId: item.CreatedByAdminID,
		CreatedAtMs:      item.CreatedAtMs,
		UpdatedAtMs:      item.UpdatedAtMs,
		Pool:             toProtoPrettyDisplayIDPool(item.Pool),
	}
}

func toProtoPrettyDisplayIDBatch(batch userdomain.PrettyDisplayIDGenerationBatch) *userv1.PrettyDisplayIDGenerationBatch {
	// 批次响应保留 requested/generated/skipped 三个计数，后台可以直接解释生成不足是否来自冲突。
	return &userv1.PrettyDisplayIDGenerationBatch{
		AppCode:              batch.AppCode,
		BatchId:              batch.BatchID,
		PoolId:               batch.PoolID,
		RuleType:             batch.RuleType,
		RuleConfigJson:       batch.RuleConfigJSON,
		RequestedCount:       batch.RequestedCount,
		GeneratedCount:       batch.GeneratedCount,
		SkippedConflictCount: batch.SkippedConflictCount,
		Status:               batch.Status,
		OperatorAdminId:      batch.OperatorAdminID,
		RequestId:            batch.RequestID,
		CreatedAtMs:          batch.CreatedAtMs,
		UpdatedAtMs:          batch.UpdatedAtMs,
	}
}

// toProtoStatus 固定未知状态的安全默认值。
func toProtoStatus(status userdomain.Status) userv1.UserStatus {
	// 状态枚举转换集中在这里，未知状态使用 UNSPECIFIED 安全兜底。
	switch status {
	case userdomain.StatusActive:
		return userv1.UserStatus_USER_STATUS_ACTIVE
	case userdomain.StatusDisabled:
		return userv1.UserStatus_USER_STATUS_DISABLED
	case userdomain.StatusBanned:
		return userv1.UserStatus_USER_STATUS_BANNED
	default:
		return userv1.UserStatus_USER_STATUS_UNSPECIFIED
	}
}

func fromProtoStatus(status userv1.UserStatus) userdomain.Status {
	switch status {
	case userv1.UserStatus_USER_STATUS_ACTIVE:
		return userdomain.StatusActive
	case userv1.UserStatus_USER_STATUS_DISABLED:
		return userdomain.StatusDisabled
	case userv1.UserStatus_USER_STATUS_BANNED:
		return userdomain.StatusBanned
	default:
		return ""
	}
}

// authMeta 提取认证链路审计所需的 gateway 透传字段。
func authMeta(meta *userv1.RequestMeta) authservice.Meta {
	if meta == nil {
		// meta 可选，缺失时审计字段为空但不能让 RPC panic。
		return authservice.Meta{}
	}

	// 只提取认证审计需要的字段，device_id 在各 RPC 中单独传入。
	return authservice.Meta{
		AppCode:            meta.GetAppCode(),
		RequestID:          meta.GetRequestId(),
		ClientIP:           meta.GetClientIp(),
		UserAgent:          meta.GetUserAgent(),
		CountryByIP:        meta.GetCountryByIp(),
		Platform:           meta.GetPlatform(),
		AppVersion:         meta.GetAppVersion(),
		BuildNumber:        meta.GetBuildNumber(),
		Language:           meta.GetLanguage(),
		Timezone:           meta.GetTimezone(),
		DeviceModel:        meta.GetDeviceModel(),
		DeviceManufacturer: meta.GetDeviceManufacturer(),
		OSVersion:          meta.GetOsVersion(),
		IMEI:               meta.GetImei(),
	}
}

// toProtoHostProfile 把 host profile 事实转换为内部 gRPC 投影。
func toProtoHostProfile(profile hostdomain.HostProfile) *userv1.HostProfile {
	if profile.UserID == 0 {
		return nil
	}
	return &userv1.HostProfile{
		UserId:                   profile.UserID,
		Status:                   profile.Status,
		RegionId:                 profile.RegionID,
		CurrentAgencyId:          profile.CurrentAgencyID,
		CurrentMembershipId:      profile.CurrentMembershipID,
		CurrentAgencyOwnerUserId: profile.CurrentAgencyOwnerUserID,
		Source:                   profile.Source,
		FirstBecameHostAtMs:      profile.FirstBecameHostAtMs,
		CreatedAtMs:              profile.CreatedAtMs,
		UpdatedAtMs:              profile.UpdatedAtMs,
	}
}

// toProtoAgency 把 Agency 事实转换为内部 gRPC 投影。
func toProtoAgency(agency hostdomain.Agency) *userv1.Agency {
	if agency.AgencyID == 0 {
		return nil
	}
	return &userv1.Agency{
		AgencyId:        agency.AgencyID,
		OwnerUserId:     agency.OwnerUserID,
		RegionId:        agency.RegionID,
		ParentBdUserId:  agency.ParentBDUserID,
		Name:            agency.Name,
		Status:          agency.Status,
		JoinEnabled:     agency.JoinEnabled,
		MaxHosts:        agency.MaxHosts,
		CreatedByUserId: agency.CreatedByUserID,
		CreatedAtMs:     agency.CreatedAtMs,
		UpdatedAtMs:     agency.UpdatedAtMs,
		Avatar:          agency.Avatar,
		HostCount:       agency.ActiveHostCount,
	}
}

// toProtoBDProfile 把 BD profile 事实转换为内部 gRPC 投影。
func toProtoBDProfile(profile hostdomain.BDProfile) *userv1.BDProfile {
	if profile.UserID == 0 {
		return nil
	}
	return &userv1.BDProfile{
		UserId:             profile.UserID,
		Role:               profile.Role,
		RegionId:           profile.RegionID,
		ParentLeaderUserId: profile.ParentLeaderUserID,
		ParentOwnerUserId:  profile.ParentOwnerUserID,
		Status:             profile.Status,
		CreatedByUserId:    profile.CreatedByUserID,
		CreatedAtMs:        profile.CreatedAtMs,
		UpdatedAtMs:        profile.UpdatedAtMs,
	}
}

// toProtoCoinSellerProfile 把币商身份事实转换为内部 gRPC 投影。
func toProtoCoinSellerProfile(profile hostdomain.CoinSellerProfile) *userv1.CoinSellerProfile {
	if profile.UserID == 0 {
		return nil
	}
	return &userv1.CoinSellerProfile{
		UserId:                  profile.UserID,
		Status:                  profile.Status,
		MerchantAssetType:       profile.MerchantAssetType,
		CanManageSubCoinSellers: profile.CanManageSubCoinSellers,
		CreatedByUserId:         profile.CreatedByUserID,
		CreatedAtMs:             profile.CreatedAtMs,
		UpdatedAtMs:             profile.UpdatedAtMs,
	}
}

// toProtoCoinSellerListItem 把区域币商列表项转换为 App 读取投影。
func toProtoCoinSellerListItem(item hostdomain.CoinSellerListItem) *userv1.CoinSellerListItem {
	if item.UserID == 0 {
		return nil
	}
	return &userv1.CoinSellerListItem{
		UserId:                  item.UserID,
		DisplayUserId:           item.DisplayUserID,
		Username:                item.Username,
		Avatar:                  item.Avatar,
		CountryId:               item.CountryID,
		CountryCode:             item.CountryCode,
		CountryName:             item.CountryName,
		CountryDisplayName:      item.CountryDisplayName,
		RegionId:                item.RegionID,
		RegionCode:              item.RegionCode,
		RegionName:              item.RegionName,
		Status:                  item.Status,
		MerchantAssetType:       item.MerchantAssetType,
		ContactInfo:             item.ContactInfo,
		CanManageSubCoinSellers: item.CanManageSubCoinSellers,
		UpdatedAtMs:             item.UpdatedAtMs,
	}
}

// toProtoCoinSellerSubRelation 把父子币商关系事实转换为内部 gRPC 投影。
func toProtoCoinSellerSubRelation(relation hostdomain.CoinSellerSubRelation) *userv1.CoinSellerSubRelation {
	if relation.RelationID == 0 {
		return nil
	}
	return &userv1.CoinSellerSubRelation{
		RelationId:      relation.RelationID,
		ParentUserId:    relation.ParentUserID,
		ChildUserId:     relation.ChildUserID,
		Status:          relation.Status,
		CreatedByUserId: relation.CreatedByUserID,
		CreatedAtMs:     relation.CreatedAtMs,
		UpdatedAtMs:     relation.UpdatedAtMs,
	}
}

// toProtoCoinSellerSubApplication 把子币商审核申请转换为内部 gRPC 投影。
func toProtoCoinSellerSubApplication(application hostdomain.CoinSellerSubApplication) *userv1.CoinSellerSubApplication {
	if application.ApplicationID == 0 {
		return nil
	}
	return &userv1.CoinSellerSubApplication{
		ApplicationId:     application.ApplicationID,
		CommandId:         application.CommandID,
		ParentUserId:      application.ParentUserID,
		TargetUserId:      application.TargetUserID,
		Status:            application.Status,
		RelationId:        application.RelationID,
		ReviewedByAdminId: application.ReviewedByAdminID,
		ReviewReason:      application.ReviewReason,
		ReviewedAtMs:      application.ReviewedAtMs,
		CreatedAtMs:       application.CreatedAtMs,
		UpdatedAtMs:       application.UpdatedAtMs,
	}
}

// toProtoUserRoleSummary 把 host domain 入口显隐摘要转换为 gRPC 投影。
func toProtoUserRoleSummary(summary hostdomain.UserRoleSummary) *userv1.UserRoleSummary {
	if summary.UserID == 0 {
		return nil
	}
	return &userv1.UserRoleSummary{
		UserId:                 summary.UserID,
		IsHost:                 summary.IsHost,
		IsAgency:               summary.IsAgency,
		IsManager:              summary.IsManager,
		IsBd:                   summary.IsBD,
		IsBdLeader:             summary.IsBDLeader,
		IsCoinSeller:           summary.IsCoinSeller,
		HostStatus:             summary.HostStatus,
		AgencyId:               summary.AgencyID,
		BdId:                   summary.BDID,
		BdStatus:               summary.BDStatus,
		CoinSellerStatus:       summary.CoinSellerStatus,
		PendingRoleInvitations: summary.PendingRoleInvitations,
		Roles:                  roleNamesFromSummary(summary),
	}
}

func roleNamesFromSummary(summary hostdomain.UserRoleSummary) []string {
	roles := make([]string, 0, 6)
	if summary.IsHost {
		roles = append(roles, userdomain.AdminRoleHost)
	}
	if summary.IsAgency {
		roles = append(roles, userdomain.AdminRoleAgency)
	}
	if summary.IsBD {
		roles = append(roles, userdomain.AdminRoleBD)
	}
	if summary.IsBDLeader {
		roles = append(roles, userdomain.AdminRoleBDLeader)
	}
	if summary.IsCoinSeller {
		roles = append(roles, userdomain.AdminRoleCoinSeller)
	}
	if summary.IsManager {
		roles = append(roles, userdomain.AdminRoleManager)
	}
	return roles
}

// toProtoAgencyMembership 把 Agency membership 事实转换为内部 gRPC 投影。
func toProtoAgencyMembership(membership hostdomain.AgencyMembership) *userv1.AgencyMembership {
	if membership.MembershipID == 0 {
		return nil
	}
	return &userv1.AgencyMembership{
		MembershipId:   membership.MembershipID,
		AgencyId:       membership.AgencyID,
		HostUserId:     membership.HostUserID,
		RegionId:       membership.RegionID,
		MembershipType: membership.MembershipType,
		Status:         membership.Status,
		JoinedAtMs:     membership.JoinedAtMs,
		EndedAtMs:      membership.EndedAtMs,
		EndedByUserId:  membership.EndedByUserID,
		EndedReason:    membership.EndedReason,
		CreatedAtMs:    membership.CreatedAtMs,
		UpdatedAtMs:    membership.UpdatedAtMs,
	}
}

// toProtoAgencyApplication 把 Agency application 事实转换为内部 gRPC 投影。
func toProtoAgencyApplication(application hostdomain.AgencyApplication) *userv1.AgencyApplication {
	if application.ApplicationID == 0 {
		return nil
	}
	return &userv1.AgencyApplication{
		ApplicationId:    application.ApplicationID,
		CommandId:        application.CommandID,
		ApplicantUserId:  application.ApplicantUserID,
		AgencyId:         application.AgencyID,
		RegionId:         application.RegionID,
		Status:           application.Status,
		ReviewedByUserId: application.ReviewedByUserID,
		ReviewReason:     application.ReviewReason,
		ReviewedAtMs:     application.ReviewedAtMs,
		CreatedAtMs:      application.CreatedAtMs,
		UpdatedAtMs:      application.UpdatedAtMs,
	}
}

// toProtoRoleInvitation 把角色邀请事实转换为内部 gRPC 投影。
func toProtoRoleInvitation(invitation hostdomain.RoleInvitation) *userv1.RoleInvitation {
	if invitation.InvitationID == 0 {
		return nil
	}
	return &userv1.RoleInvitation{
		InvitationId:           invitation.InvitationID,
		CommandId:              invitation.CommandID,
		InvitationType:         invitation.InvitationType,
		Status:                 invitation.Status,
		InviterUserId:          invitation.InviterUserID,
		InviterBdUserId:        invitation.InviterBDUserID,
		TargetUserId:           invitation.TargetUserID,
		ExternalOperatorUserId: invitation.ExternalOperatorUserID,
		RegionId:               invitation.RegionID,
		AgencyName:             invitation.AgencyName,
		ParentBdUserId:         invitation.ParentBDUserID,
		ParentLeaderUserId:     invitation.ParentLeaderUserID,
		ParentOwnerUserId:      invitation.ParentOwnerUserID,
		ParentAgencyId:         invitation.ParentAgencyID,
		ProcessedByUserId:      invitation.ProcessedByUserID,
		ProcessReason:          invitation.ProcessReason,
		ProcessedAtMs:          invitation.ProcessedAtMs,
		CreatedAtMs:            invitation.CreatedAtMs,
		UpdatedAtMs:            invitation.UpdatedAtMs,
	}
}

// toProtoExternalTeamUser 保留长 ID、默认短号和当前短号，避免外管列表再回查或把靓号误当内部 ID。
func toProtoExternalTeamUser(user hostdomain.ExternalTeamUser) *userv1.ExternalTeamUser {
	if user.UserID == 0 {
		return nil
	}
	return &userv1.ExternalTeamUser{
		UserId:               user.UserID,
		DefaultDisplayUserId: user.DefaultDisplayUserID,
		CurrentDisplayUserId: user.CurrentDisplayUserID,
		Username:             user.Username,
		Avatar:               user.Avatar,
		Status:               user.Status,
		RegionId:             user.RegionID,
		Roles:                append([]string(nil), user.Roles...),
		ParentOwnerUserId:    user.ParentOwnerUserID,
		ParentBdUserId:       user.ParentBDUserID,
		AgencyId:             user.AgencyID,
	}
}
