// Package user 实现用户主数据、展示短号和临时靓号用例。
package user

import (
	"context"
	"time"

	roomv1 "hyapp.local/api/proto/room/v1"
	"hyapp/pkg/idgen"
	hostdomain "hyapp/services/user-service/internal/domain/host"
	invitedomain "hyapp/services/user-service/internal/domain/invite"
	userdomain "hyapp/services/user-service/internal/domain/user"
	hostservice "hyapp/services/user-service/internal/service/host"
)

// UserRepository 隔离用户主数据读写。
// 它只覆盖 users 主表视角和“创建用户+默认短号”的事务入口，避免查询用例依赖短号变更细节。
type UserRepository interface {
	// GetUser 查询单个用户主状态。
	GetUser(ctx context.Context, userID int64) (userdomain.User, error)
	// GetMyProfileStats 读取我的页顶部统计 read model。
	GetMyProfileStats(ctx context.Context, userID int64) (userdomain.ProfileStats, error)
	// RecordProfileVisit 记录一次用户主页访问，并更新被访问者计数。
	RecordProfileVisit(ctx context.Context, visitorUserID int64, targetUserID int64, nowMs int64) (bool, userdomain.ProfileStats, error)
	// ListProfileVisitors 分页读取访问当前用户主页的人。
	ListProfileVisitors(ctx context.Context, userID int64, page int32, pageSize int32) ([]userdomain.ProfileVisitRecord, int64, error)
	// FollowUser 创建或恢复单向关注，并更新关注计数。
	FollowUser(ctx context.Context, followerUserID int64, followeeUserID int64, nowMs int64) (userdomain.ProfileStats, error)
	// UnfollowUser 取消单向关注，并更新关注计数。
	UnfollowUser(ctx context.Context, followerUserID int64, followeeUserID int64, nowMs int64) (userdomain.ProfileStats, error)
	// ListFollowing 读取当前用户关注的人；公开列表使用 page/pageSize，房间关系流可用 cursor 做稳定续扫。
	ListFollowing(ctx context.Context, userID int64, page int32, pageSize int32, cursorUpdatedAtMS int64, cursorUserID int64) ([]userdomain.FollowRecord, int64, error)
	// ApplyFriend 创建好友申请；已有好友时不重复建申请。
	ApplyFriend(ctx context.Context, requesterUserID int64, targetUserID int64, nowMs int64) (userdomain.FriendApplication, bool, error)
	// AcceptFriendApplication 同意指定 requester 发来的好友申请，并写双向好友关系。
	AcceptFriendApplication(ctx context.Context, accepterUserID int64, requesterUserID int64, nowMs int64) (userdomain.FriendRecord, error)
	// DeleteFriend 删除双向好友关系。
	DeleteFriend(ctx context.Context, userID int64, friendUserID int64, nowMs int64) (bool, error)
	// ListFriends 读取好友列表；公开列表使用 page/pageSize，房间关系流可用 cursor 做稳定续扫。
	ListFriends(ctx context.Context, userID int64, page int32, pageSize int32, cursorUpdatedAtMS int64, cursorUserID int64) ([]userdomain.FriendRecord, int64, error)
	// ListFriendApplications 分页读取好友申请。
	ListFriendApplications(ctx context.Context, userID int64, direction string, page int32, pageSize int32) ([]userdomain.FriendApplication, int64, error)
	// SubmitReport 持久化 App 用户提交的治理举报单。
	SubmitReport(ctx context.Context, report userdomain.Report) (userdomain.Report, error)
	// BusinessUserLookup 按已授权业务场景查询目标用户，返回字段必须保持最小化。
	BusinessUserLookup(ctx context.Context, keyword string, numericKeyword bool, pageSize int32) ([]userdomain.BusinessUserLookupItem, error)
	// BatchGetUsers 批量查询用户主状态，缺失用户不应返回占位对象。
	BatchGetUsers(ctx context.Context, userIDs []int64) (map[int64]userdomain.User, error)
	// BatchGetUserAdminProfiles 批量读取后台列表需要的用户、身份、登录和封禁 owner 事实。
	BatchGetUserAdminProfiles(ctx context.Context, userIDs []int64, nowMs int64) (map[int64]userdomain.AdminProfile, error)
	// BatchGetRoomBasicUsers 批量查询房间首屏最小用户资料，不触发完整用户主状态投影。
	BatchGetRoomBasicUsers(ctx context.Context, userIDs []int64) (map[int64]userdomain.RoomBasicUser, error)
	// GetInviteAttribution 按被邀请用户读取邀请归因快照；无关系时返回 Found=false。
	GetInviteAttribution(ctx context.Context, invitedUserID int64) (invitedomain.Attribution, error)
	// FindInviteReferrerByCode 按 active 邀请码读取归属用户，供 H5 绑定页搜索同区域邀请人。
	FindInviteReferrerByCode(ctx context.Context, appCode string, inviteCode string) (userdomain.User, error)
	// ListUserIDs 按稳定 user_id 游标读取低频后台任务目标用户。
	ListUserIDs(ctx context.Context, filter userdomain.UserIDPageFilter) ([]int64, error)
	// CreateUserWithIdentity 原子创建用户和默认短号。
	CreateUserWithIdentity(ctx context.Context, createdUser userdomain.User, identity userdomain.Identity) error
	// UpdateUserProfile 原子修改用户基础资料。
	UpdateUserProfile(ctx context.Context, command userdomain.ProfileUpdateCommand) (userdomain.User, error)
	// UpdateUserProfileBackground 原子修改个人信息页背景图。
	UpdateUserProfileBackground(ctx context.Context, command userdomain.ProfileBackgroundUpdateCommand) (userdomain.User, error)
	// UpdateUserContactInfo 原子修改用户级联系方式。
	UpdateUserContactInfo(ctx context.Context, command userdomain.ProfileContactUpdateCommand) (userdomain.User, error)
	// UpdateUserWithdrawAddress 原子修改用户默认提现地址。
	UpdateUserWithdrawAddress(ctx context.Context, command userdomain.WithdrawAddressUpdateCommand) (userdomain.User, error)
	// CompleteOnboarding 原子写注册页必填资料并标记 profile_completed。
	CompleteOnboarding(ctx context.Context, command userdomain.CompleteOnboardingCommand) (userdomain.User, error)
	// BindInviteReferrer 原子绑定当前用户的邀请人，并在事务内复核双方同区域和账号可用性。
	BindInviteReferrer(ctx context.Context, command userdomain.BindInviteReferrerCommand) (userdomain.User, error)
	// ChangeUserCountry 原子修改用户国家、写变更日志，并返回需要立即失效的 auth session。
	ChangeUserCountry(ctx context.Context, command userdomain.CountryChangeCommand) (userdomain.User, int64, []string, error)
}

// PrivacyBenefitChecker 只暴露 user-service 执行资料隐私所需的单项 VIP 权益判断。
// 具体权益、体验卡排除项和用户开关均由 wallet-service 统一计算，user-service 不复制 VIP 规则。
type PrivacyBenefitChecker interface {
	CheckVipBenefit(ctx context.Context, requestID string, userID int64, benefitCode string) (bool, error)
}

// VIPBenefitDecision 是 wallet 对 App VIP 策略和单项权益计算后的最小结果。
// user-service 只消费 program_type 与授权等级事实，不复制 Fami 的等级配置。
type VIPBenefitDecision struct {
	ProgramType           string
	Allowed               bool
	RequiredLevel         int32
	CurrentEffectiveLevel int32
}

// VIPBenefitDecisionChecker 为头像上传门禁保留升级页所需的结构化权益事实。
type VIPBenefitDecisionChecker interface {
	CheckVIPBenefitDecision(ctx context.Context, requestID string, userID int64, benefitCode string) (VIPBenefitDecision, error)
}

// AvatarUploadRepository 持久化头像上传授权，并在 users 更新事务里单次消费凭证。
// 独立于 UserRepository 可避免基础测试替身被迫实现媒体表方法；生产 MySQL repository 必须实现。
type AvatarUploadRepository interface {
	GetAvatarUpload(ctx context.Context, userID int64, uploadID string) (userdomain.AvatarUploadRegistration, bool, error)
	RegisterAvatarUpload(ctx context.Context, registration userdomain.AvatarUploadRegistration) (userdomain.AvatarUploadRegistration, error)
	CompleteAvatarUpload(ctx context.Context, registration userdomain.AvatarUploadRegistration) (userdomain.AvatarUploadRegistration, error)
}

// AppRegistryRepository 负责把客户端包名解析成内部 app_code。
type AppRegistryRepository interface {
	ResolveApp(ctx context.Context, appCode string, packageName string, platform string) (userdomain.App, error)
	ListApps(ctx context.Context) ([]userdomain.App, error)
}

// CountryRegionRepository 负责把用户选择的 country_code 解析成国家主数据和当前 active 区域。
// 注册和改国家都依赖这组同步读，避免未知国家码进入 users 主表。
type CountryRegionRepository interface {
	// ResolveEnabledCountryByCode 按国家码读取 App 可选国家；必须存在且 enabled=true。
	ResolveEnabledCountryByCode(ctx context.Context, countryCode string) (userdomain.Country, bool, error)
	// ListRegistrationCountries 返回 App 注册页可选国家列表。
	ListRegistrationCountries(ctx context.Context) ([]userdomain.Country, error)
	// ResolveActiveRegionByCountry 按国家码读取当前 active 区域；无显式映射时返回 GLOBAL 兜底区域。
	ResolveActiveRegionByCountry(ctx context.Context, countryCode string) (userdomain.Region, bool, error)
}

// CountryAdminRepository 是国家管理 RPC 的持久化边界。
type CountryAdminRepository interface {
	CountryRegionRepository
	UpdateCountry(ctx context.Context, command userdomain.UpdateCountryCommand) (userdomain.Country, error)
	ListCountries(ctx context.Context, filter userdomain.CountryFilter) ([]userdomain.Country, error)
}

// RegionAdminRepository 是区域管理 RPC 的持久化边界。
type RegionAdminRepository interface {
	CountryRegionRepository
	UpdateRegion(ctx context.Context, command userdomain.UpdateRegionCommand) (userdomain.Region, error)
	ListRegions(ctx context.Context, filter userdomain.RegionFilter) ([]userdomain.Region, error)
	GetRegion(ctx context.Context, regionID int64) (userdomain.Region, error)
	ReplaceRegionCountries(ctx context.Context, command userdomain.ReplaceRegionCountriesCommand) (userdomain.Region, error)
}

// RegionRebuildRepository 负责消费 user_region_rebuild_tasks 并分页刷新 users.region_id。
type RegionRebuildRepository interface {
	// ProcessNextRegionRebuildTask 先持久化 claim，再处理一个待重算任务；没有任务时返回 NoTask=true。
	ProcessNextRegionRebuildTask(ctx context.Context, workerID string, nowMs int64, lockTTL time.Duration, batchSize int) (userdomain.RegionRebuildProcessResult, error)
}

// RoleScopePolicyResolver 从 user-service owner 配置解析 App 角色范围；数据库故障必须返回错误而不是扩大治理权限。
type RoleScopePolicyResolver interface {
	ResolveRoleScopePolicy(ctx context.Context, scene string) (hostservice.RoleScopePolicy, error)
}

// RoleSummaryRepository 提供用户当前业务身份摘要，App 自助改国家必须先基于该事实做权限截断。
type RoleSummaryRepository interface {
	GetUserRoleSummary(ctx context.Context, userID int64) (hostdomain.UserRoleSummary, error)
}

// IdentityRepository 隔离 display_user_id 的归属、解析、变更和靓号租约。
// 用户 service 通过该接口表达短号用例，不直接感知 MySQL 的 identity/change_log/lease 表布局。
type IdentityRepository interface {
	// GetUserIdentity 按 user_id 读取当前 active display_user_id。
	GetUserIdentity(ctx context.Context, userID int64) (userdomain.Identity, error)
	// ResolveDisplayUserID 把短号解析成 user_id，并按 nowMs 触发懒过期。
	ResolveDisplayUserID(ctx context.Context, displayUserID string, nowMs int64) (userdomain.Identity, error)
	// ResolveAdminUserIdentifier 为内部后台精确解析永久默认短号、当前有效展示号或系统长 user_id。
	ResolveAdminUserIdentifier(ctx context.Context, userIdentifier string, nowMs int64) (int64, error)
	// ChangeDisplayUserID 原子修改默认短号并写变更日志。
	ChangeDisplayUserID(ctx context.Context, command userdomain.DisplayUserIDChangeCommand) (userdomain.Identity, error)
	// ApplyPrettyDisplayUserID 原子创建靓号租约并覆盖当前展示号。
	ApplyPrettyDisplayUserID(ctx context.Context, command userdomain.PrettyDisplayUserIDCommand) (userdomain.Identity, string, error)
	// ListAvailablePrettyDisplayIDs 读取当前用户等级可申请的靓号池号码。
	ListAvailablePrettyDisplayIDs(ctx context.Context, query userdomain.AvailablePrettyDisplayIDQuery) ([]userdomain.PrettyDisplayID, int64, error)
	// ApplyPrettyDisplayIDFromPool 原子申请靓号池号码并长期覆盖当前展示号。
	ApplyPrettyDisplayIDFromPool(ctx context.Context, command userdomain.PrettyDisplayIDPoolApplyCommand) (userdomain.Identity, error)
	// ListPrettyDisplayIDPools 分页读取后台靓号池配置。
	ListPrettyDisplayIDPools(ctx context.Context, query userdomain.PrettyDisplayIDPoolQuery) ([]userdomain.PrettyDisplayIDPool, int64, error)
	// CreatePrettyDisplayIDPool 创建后台靓号池配置。
	CreatePrettyDisplayIDPool(ctx context.Context, command userdomain.PrettyDisplayIDPoolCommand) (userdomain.PrettyDisplayIDPool, error)
	// UpdatePrettyDisplayIDPool 更新后台靓号池配置。
	UpdatePrettyDisplayIDPool(ctx context.Context, command userdomain.PrettyDisplayIDPoolCommand) (userdomain.PrettyDisplayIDPool, error)
	// GeneratePrettyDisplayIDs 按靓号池规则批量生成号码。
	GeneratePrettyDisplayIDs(ctx context.Context, command userdomain.PrettyDisplayIDGenerateCommand) (userdomain.PrettyDisplayIDGenerationBatch, error)
	// ListPrettyDisplayIDs 分页读取后台靓号列表。
	ListPrettyDisplayIDs(ctx context.Context, query userdomain.PrettyDisplayIDQuery) ([]userdomain.PrettyDisplayID, int64, error)
	// SetPrettyDisplayIDStatus 切换未占用靓号的状态。
	SetPrettyDisplayIDStatus(ctx context.Context, command userdomain.PrettyDisplayIDStatusCommand) (userdomain.PrettyDisplayID, error)
	// RecyclePrettyDisplayID 后台强制回收用户当前占用的靓号。
	RecyclePrettyDisplayID(ctx context.Context, command userdomain.PrettyDisplayIDRecycleCommand) (userdomain.PrettyDisplayID, error)
	// AdminGrantPrettyDisplayID 后台直接发放靓号并覆盖当前展示号。
	AdminGrantPrettyDisplayID(ctx context.Context, command userdomain.AdminGrantPrettyDisplayIDCommand) (userdomain.Identity, string, error)
	// ExpirePrettyDisplayUserID 原子恢复默认短号。
	ExpirePrettyDisplayUserID(ctx context.Context, command userdomain.ExpirePrettyDisplayUserIDCommand) (userdomain.Identity, error)
}

// LevelProfileReader 读取用户当前等级投影，供靓号池申请做准入校验。
type LevelProfileReader interface {
	GetUserLevelProfile(ctx context.Context, appCode string, userID int64, requestID string) (userdomain.UserLevelProfile, error)
}

// DeviceRepository 隔离设备 push token 的持久化。
// 推送 token 只服务离线触达，不进入认证、房间或钱包主链路。
type DeviceRepository interface {
	// BindPushToken 原子绑定或替换当前用户设备 token。
	BindPushToken(ctx context.Context, command userdomain.PushTokenCommand) error
	// DeletePushToken 失效当前用户设备 token。
	DeletePushToken(ctx context.Context, command userdomain.DeletePushTokenCommand) (bool, error)
}

// ContentBlacklistRepository 持有运营内容屏蔽当前态。
// 它与账号封禁分离：黑名单只改变游戏列表和 Banner 可见性，不能吊销登录态或修改 users.status。
type ContentBlacklistRepository interface {
	IsUserContentBlacklisted(ctx context.Context, userID int64) (bool, error)
	ListUserContentBlacklist(ctx context.Context, page int32, pageSize int32) ([]userdomain.ContentBlacklistEntry, int64, error)
	AddUserContentBlacklist(ctx context.Context, command UserContentBlacklistCommand) (userdomain.ContentBlacklistEntry, error)
	RemoveUserContentBlacklist(ctx context.Context, command UserContentBlacklistCommand) (bool, error)
}

// ModerationRepository 持有用户状态和认证 session 的同库事务。
type ModerationRepository interface {
	SetUserStatus(ctx context.Context, command UserStatusCommand) (UserStatusPersistenceResult, error)
	ManagerCrossRegionEnabled(ctx context.Context, managerUserID int64) (bool, error)
	CreateManagerUserBlock(ctx context.Context, command ManagerUserBlockCommand, target userdomain.User) (ManagerUserBlock, error)
	ListManagerUserBlocks(ctx context.Context, managerUserID int64, status string, pageSize int32) ([]ManagerUserBlock, error)
	ReleaseManagerUserBlock(ctx context.Context, command ReleaseManagerUserBlockCommand) (ManagerUserBlockRelease, error)
	ExpireManagerUserBlocks(ctx context.Context, nowMS int64, limit int32) ([]ManagerUserBlockRelease, error)
	CreateAdminUserBan(ctx context.Context, command AdminUserBanCommand) (AdminUserBanPersistenceResult, error)
	ReleaseAdminUserBan(ctx context.Context, command ReleaseAdminUserBanCommand) (AdminUserBanRelease, error)
	// ForceReleaseUserBans 是后台管理员的最高治理入口，必须在一个事务内释放管理员和经理两类封禁事实。
	ForceReleaseUserBans(ctx context.Context, command AdminForceUnbanCommand) (AdminForceUnbanResult, error)
	ExpireAdminUserBans(ctx context.Context, nowMS int64, limit int32) ([]AdminUserBanRelease, error)
}

// SessionDenylist 写入 gateway access token 快速拒绝缓存。
type SessionDenylist interface {
	SetRevokedSession(ctx context.Context, appCode string, sessionID string, reason string, ttl time.Duration) error
}

// IMLoginKicker 失效用户在腾讯云 IM SDK 的当前登录态。
type IMLoginKicker interface {
	KickUser(ctx context.Context, userID int64) error
}

// RoomEvictor 通过 room-service 驱逐用户当前所在的 Room Cell。
type RoomEvictor interface {
	SystemEvictUser(ctx context.Context, req *roomv1.SystemEvictUserRequest) (*roomv1.SystemEvictUserResponse, error)
}

// IDGenerator 生成系统内部不可变 user_id。
type IDGenerator interface {
	// NewInt64 返回全局唯一 user_id。
	NewInt64() int64
}

// DisplayUserIDAllocator 为新用户生成默认短号候选。
type DisplayUserIDAllocator interface {
	// NextDisplayUserID 根据 user_id 和重试次数生成候选短号。
	NextDisplayUserID(userID int64, attempt int) string
}

// Option 调整 user service 的运行时策略，测试可注入确定性发号器。
type Option func(*Service)

// Service 承载用户主数据和短号用例。
// 字段按领域接口拆分，跨表事务仍保留在 repository 方法内，避免 service 层手动拼事务。
type Service struct {
	// appRegistryRepository 持有 App 注册表读取能力，gateway 解析 app_code 时使用。
	appRegistryRepository AppRegistryRepository
	// userRepository 持有 users 主表视角的读写能力。
	userRepository UserRepository
	// privacyBenefitChecker 实时读取 wallet-service 计算后的隐私权益；RPC 失败时业务层按保护隐私处理。
	privacyBenefitChecker PrivacyBenefitChecker
	// vipBenefitDecisionChecker 执行动态头像精确权益门禁并提供 required/current level 元数据。
	vipBenefitDecisionChecker VIPBenefitDecisionChecker
	// avatarUploadRepository 保存上传授权；资料更新消费仍由 userRepository 的同库事务完成。
	avatarUploadRepository AvatarUploadRepository
	// identityRepository 持有短号、靓号和变更日志事务能力。
	identityRepository IdentityRepository
	// levelProfileReader 读取 activity-service 等级投影，靓号池申请必须依赖它判断等级区间。
	levelProfileReader LevelProfileReader
	// countryRegionRepository 持有国家主数据校验和国家到区域映射解析能力。
	countryRegionRepository CountryRegionRepository
	// countryAdminRepository 持有国家管理事务能力。
	countryAdminRepository CountryAdminRepository
	// regionAdminRepository 持有区域管理事务能力。
	regionAdminRepository RegionAdminRepository
	// regionRebuildRepository 持有历史用户区域快照重算能力。
	regionRebuildRepository RegionRebuildRepository
	// roleSummaryRepository 持有用户当前业务身份摘要，避免 App 自助改国家绕过 host/BD/币商等身份边界。
	roleSummaryRepository RoleSummaryRepository
	// deviceRepository 持有设备 push token 绑定能力。
	deviceRepository DeviceRepository
	// contentBlacklistRepository 持有游戏列表和 Banner 屏蔽当前态，不参与账号封禁或登录状态判断。
	contentBlacklistRepository ContentBlacklistRepository
	// moderationRepository 持有用户状态和 session 吊销事务能力。
	moderationRepository ModerationRepository
	// roleScopePolicyResolver 与 Host 组织链路共享同一 owner 配置，经理治理不能复制 App 分支。
	roleScopePolicyResolver RoleScopePolicyResolver
	// sessionDenylist 让已签发 access token 在 gateway 立即失效。
	sessionDenylist SessionDenylist
	// sessionDenylistTTL 覆盖 access token 剩余有效期，避免 Redis key 先于 JWT 过期。
	sessionDenylistTTL time.Duration
	// imLoginKicker 失效腾讯云 IM 当前登录态。
	imLoginKicker IMLoginKicker
	// roomEvictor 通过 room-service 清理当前房间 presence、麦位和 RTC 连接。
	roomEvictor RoomEvictor
	// idGenerator 生成系统内部不可变 user_id。
	idGenerator IDGenerator
	// displayUserIDAllocator 生成默认短号候选，唯一性由 repository 保证。
	displayUserIDAllocator DisplayUserIDAllocator
	// now 是服务时钟，测试可注入固定时间。
	now func() time.Time
	// allocateMaxAttempts 是默认短号冲突重试上限。
	allocateMaxAttempts int
	// displayUserIDCooldown 是用户修改默认短号的冷却期。
	displayUserIDCooldown time.Duration
	// countryChangeCooldown 是用户修改国家的滚动冷却期。
	countryChangeCooldown time.Duration
}

// New 创建用户服务。
// 构造函数只强制要求 UserRepository；如果同一实现也支持 IdentityRepository，则自动接入短号用例。
func New(userRepository UserRepository, options ...Option) *Service {
	// 默认策略可独立跑通本地单测；生产 repository 由 app 层注入。
	svc := &Service{
		userRepository:         userRepository,
		idGenerator:            idgen.NewInt64Generator(0),
		displayUserIDAllocator: NumericDisplayUserIDAllocator{},
		now:                    time.Now,
		allocateMaxAttempts:    8,
		displayUserIDCooldown:  30 * 24 * time.Hour,
		countryChangeCooldown:  30 * 24 * time.Hour,
	}
	if repository, ok := userRepository.(ModerationRepository); ok {
		// MySQL 用户 repository 同时持有 auth_sessions 表的同库事务能力。
		svc.moderationRepository = repository
	}
	if repository, ok := userRepository.(AvatarUploadRepository); ok {
		// 生产 MySQL 用户 repository 同时拥有 users 与头像授权表，凭证消费才能和资料更新保持原子。
		svc.avatarUploadRepository = repository
	}
	if repository, ok := userRepository.(ContentBlacklistRepository); ok {
		// 生产 MySQL 用户 repository 同库维护屏蔽名单；测试替身未覆盖该能力时保持可选。
		svc.contentBlacklistRepository = repository
	}
	for _, option := range options {
		// option 只改依赖或策略，不做 IO。
		option(svc)
	}

	return svc
}

// WithPrivacyBenefitChecker 注入 wallet-service 的最终 VIP 权益判断。
func WithPrivacyBenefitChecker(checker PrivacyBenefitChecker) Option {
	return func(s *Service) {
		if checker != nil {
			s.privacyBenefitChecker = checker
		}
	}
}

// WithVIPBenefitDecisionChecker 注入 wallet-service 对精确权益和 App VIP 策略的最终判断。
func WithVIPBenefitDecisionChecker(checker VIPBenefitDecisionChecker) Option {
	return func(s *Service) {
		if checker != nil {
			s.vipBenefitDecisionChecker = checker
		}
	}
}

// WithModerationRepository 注入用户治理事务 repository。
func WithModerationRepository(repository ModerationRepository) Option {
	return func(s *Service) {
		if repository != nil {
			s.moderationRepository = repository
		}
	}
}

// WithRoleScopePolicyResolver 注入 user-service 自有的角色范围解析器。
func WithRoleScopePolicyResolver(resolver RoleScopePolicyResolver) Option {
	return func(s *Service) {
		if resolver != nil {
			s.roleScopePolicyResolver = resolver
		}
	}
}

// WithSessionDenylist 注入 access session denylist 缓存。
func WithSessionDenylist(denylist SessionDenylist, ttl time.Duration) Option {
	return func(s *Service) {
		if denylist != nil {
			s.sessionDenylist = denylist
			s.sessionDenylistTTL = ttl
		}
	}
}

// WithIMLoginKicker 注入腾讯云 IM 当前登录态失效边界。
func WithIMLoginKicker(kicker IMLoginKicker) Option {
	return func(s *Service) {
		if kicker != nil {
			s.imLoginKicker = kicker
		}
	}
}

// WithRoomEvictor 注入当前房间驱逐边界。
func WithRoomEvictor(evictor RoomEvictor) Option {
	return func(s *Service) {
		if evictor != nil {
			s.roomEvictor = evictor
		}
	}
}

// WithAppRegistryRepository 注入 App 注册表 repository。
func WithAppRegistryRepository(repository AppRegistryRepository) Option {
	return func(s *Service) {
		if repository != nil {
			s.appRegistryRepository = repository
		}
	}
}

// WithCountryRegionRepository 注入国家/区域解析 repository。
func WithCountryRegionRepository(repository CountryRegionRepository) Option {
	return func(s *Service) {
		if repository != nil {
			// 注册和改国家不能在缺失主数据校验时静默写入自由文本。
			s.countryRegionRepository = repository
		}
	}
}

// WithCountryAdminRepository 注入国家管理 repository。
func WithCountryAdminRepository(repository CountryAdminRepository) Option {
	return func(s *Service) {
		if repository != nil {
			s.countryAdminRepository = repository
			s.countryRegionRepository = repository
		}
	}
}

// WithRegionAdminRepository 注入区域管理 repository。
func WithRegionAdminRepository(repository RegionAdminRepository) Option {
	return func(s *Service) {
		if repository != nil {
			s.regionAdminRepository = repository
			s.countryRegionRepository = repository
		}
	}
}

// WithRegionRebuildRepository 注入历史用户区域重算 repository。
func WithRegionRebuildRepository(repository RegionRebuildRepository) Option {
	return func(s *Service) {
		if repository != nil {
			s.regionRebuildRepository = repository
		}
	}
}

// WithRoleSummaryRepository 注入角色摘要 repository。
func WithRoleSummaryRepository(repository RoleSummaryRepository) Option {
	return func(s *Service) {
		if repository != nil {
			s.roleSummaryRepository = repository
		}
	}
}

// WithDeviceRepository 注入设备 push token repository。
func WithDeviceRepository(repository DeviceRepository) Option {
	return func(s *Service) {
		if repository != nil {
			s.deviceRepository = repository
		}
	}
}

// WithContentBlacklistRepository 注入运营内容屏蔽 repository。
func WithContentBlacklistRepository(repository ContentBlacklistRepository) Option {
	return func(s *Service) {
		if repository != nil {
			s.contentBlacklistRepository = repository
		}
	}
}

// WithCountryChangeCooldown 配置用户国家修改冷却窗口。
func WithCountryChangeCooldown(cooldown time.Duration) Option {
	return func(s *Service) {
		if cooldown >= 0 {
			// cooldown 允许为 0，便于本地测试或运营临时关闭限制。
			s.countryChangeCooldown = cooldown
		}
	}
}

// WithIdentityRepository 注入独立短号 repository。
// 该 option 支持后续把用户主数据和短号 identity 拆成不同实现，同时不改变现有 New(mysqlRepo) 调用。
func WithIdentityRepository(repository IdentityRepository) Option {
	return func(s *Service) {
		if repository != nil {
			// 允许将用户主数据和短号事务拆到不同实现。
			s.identityRepository = repository
		}
	}
}

// WithLevelProfileReader 注入 activity-service 等级查询边界。
func WithLevelProfileReader(reader LevelProfileReader) Option {
	return func(s *Service) {
		if reader != nil {
			// 等级只用于靓号池申请准入，不进入用户主表。
			s.levelProfileReader = reader
		}
	}
}

// WithIDGenerator 注入 user_id 发号器。
func WithIDGenerator(generator IDGenerator) Option {
	return func(s *Service) {
		if generator != nil {
			// nil generator 不覆盖默认发号器，避免构造出不可创建用户的 service。
			s.idGenerator = generator
		}
	}
}

// WithDisplayUserIDAllocator 注入短号候选生成器。
func WithDisplayUserIDAllocator(allocator DisplayUserIDAllocator) Option {
	return func(s *Service) {
		if allocator != nil {
			// allocator 只负责候选生成，不负责唯一性判断。
			s.displayUserIDAllocator = allocator
		}
	}
}

// WithClock 注入时间函数，保证冷却期测试不依赖真实时间。
func WithClock(now func() time.Time) Option {
	return func(s *Service) {
		if now != nil {
			// 固定时钟用于冷却期、靓号过期和创建时间测试。
			s.now = now
		}
	}
}

// WithDisplayUserIDPolicy 配置分配重试和修改冷却期。
func WithDisplayUserIDPolicy(maxAttempts int, cooldown time.Duration) Option {
	return func(s *Service) {
		if maxAttempts > 0 {
			// 只接受正数重试次数，避免配置错误导致无法创建用户。
			s.allocateMaxAttempts = maxAttempts
		}
		if cooldown >= 0 {
			// cooldown 允许为 0，便于测试和运营豁免策略。
			s.displayUserIDCooldown = cooldown
		}
	}
}
