// Package auth 实现 user-service 的认证、三方登录、密码登录和 refresh session 用例。
package auth

import (
	"context"
	"log/slog"
	"time"

	"hyapp/pkg/idgen"
	"hyapp/pkg/logx"
	"hyapp/pkg/xerr"
	authdomain "hyapp/services/user-service/internal/domain/auth"
	invitedomain "hyapp/services/user-service/internal/domain/invite"
	userdomain "hyapp/services/user-service/internal/domain/user"
	userservice "hyapp/services/user-service/internal/service/user"
)

const (
	// hashAlgBcrypt 是当前密码 hash 算法标识。
	hashAlgBcrypt = "bcrypt"
	// tokenType 是对外 token 类型。
	tokenType = "Bearer"
	// loginPassword 是密码登录审计类型。
	loginPassword = "password"
	// loginThird 是三方登录审计类型。
	loginThird = "third_party"
	// loginRefresh 是 refresh token 轮换审计类型。
	loginRefresh = "refresh"
	// loginAccessResign 是不轮换 refresh token 的 access token 重签审计类型。
	loginAccessResign = "access_resign"
	// loginSetPass 是设置密码审计类型。
	loginSetPass = "set_password"
	// resultSuccess 是审计成功结果。
	resultSuccess = "success"
	// resultFailed 是审计失败结果。
	resultFailed = "failed"
	// revokeReasonRefreshRotated 表示 refresh token 已被成功轮换，旧 access token 必须立刻失效。
	revokeReasonRefreshRotated = "REFRESH_ROTATED"
	// revokeReasonUserLogout 表示用户主动登出，旧 access token 必须立刻失效。
	revokeReasonUserLogout = "USER_LOGOUT"
)

// AuthRepository 是认证用例的持久化边界。
// 跨表写入仍按用例保留事务入口，例如三方首次登录要一次性创建用户、默认短号、三方身份和 session。
type AuthRepository interface {
	// AllocateDisplayUserID 分配当前 App 下的随机外观默认短号；最终唯一性由创建事务里的展示号事实表兜底。
	AllocateDisplayUserID(ctx context.Context, appCode string) (string, error)
	// SetPassword 为已有用户首次写入密码身份。
	SetPassword(ctx context.Context, account authdomain.PasswordAccount) error
	// CreatePasswordUser 原子创建用户、默认短号、密码身份和可选首个 session；全站机器人会传空 session。
	CreatePasswordUser(ctx context.Context, user userdomain.User, identity userdomain.Identity, account authdomain.PasswordAccount, session authdomain.Session, invite invitedomain.BindCommand, maxAccountsPerDevice int32) error
	// FindPasswordByDisplayUserID 通过当前 active 展示号或 held 默认短号登录别名读取密码身份。
	FindPasswordByDisplayUserID(ctx context.Context, displayUserID string, nowMs int64) (authdomain.PasswordAccount, error)
	// CreateSession 创建新的 refresh session。
	CreateSession(ctx context.Context, session authdomain.Session) error
	// FindActiveSessionByRefreshHash 通过 refresh token hash 查找未过期未吊销 session。
	FindActiveSessionByRefreshHash(ctx context.Context, refreshTokenHash string, nowMs int64) (authdomain.Session, error)
	// FindSessionByRefreshHash 返回包含已轮换 session 在内的完整 lineage，用于判断 grace 与 token reuse。
	FindSessionByRefreshHash(ctx context.Context, refreshTokenHash string) (authdomain.Session, error)
	// FindActiveSessionByID 通过 session_id 查找未过期未吊销 session，用于只重签 access token 的链路。
	FindActiveSessionByID(ctx context.Context, sessionID string, nowMs int64) (authdomain.Session, error)
	// FindLatestActiveSessionByUser 返回用户最新未过期未吊销 session，供后台重签 access token；无会话返回 NotFound。
	FindLatestActiveSessionByUser(ctx context.Context, userID int64, nowMs int64) (authdomain.Session, error)
	// RotateRefreshSession 原子完成旧 session 吊销、新 session 创建和加密幂等响应落库。
	RotateRefreshSession(ctx context.Context, command authdomain.RefreshRotationCommand) (authdomain.RefreshRotationResult, error)
	// RevokeSession 按 session_id 或 refresh token hash 吊销 session。
	RevokeSession(ctx context.Context, sessionID string, refreshTokenHash string, revokedAtMs int64) (bool, error)
	// RevokeSessionFamily 原子吊销显式 logout 所属 token family，并返回需要写 gateway denylist 的全部 sid。
	RevokeSessionFamily(ctx context.Context, sessionID string, refreshTokenHash string, revokedAtMs int64, denyUntilMs int64, requestID string) (string, []string, bool, error)
	// RevokeSessionWithReason 按 session_id 幂等吊销 session，并持久化撤销来源。
	RevokeSessionWithReason(ctx context.Context, sessionID string, revokedAtMs int64, reason string, requestID string, revokedBy string) (bool, error)
	// RevokeUserDeviceSessionsForRisk 按风险源 session 定位同一设备后续 session，吊销 active session，并返回需要写入 denylist 的 session_id。
	RevokeUserDeviceSessionsForRisk(ctx context.Context, sourceSessionID string, userID int64, revokedAtMs int64, reason string, requestID string, revokedBy string) ([]string, error)
	// ClaimSessionDenylistJobs 认领 family revoke 同事务写出的 Redis denylist 补偿事实。
	ClaimSessionDenylistJobs(ctx context.Context, workerID string, nowMs int64, lockUntilMs int64, limit int) ([]authdomain.SessionDenylistJob, error)
	// MarkSessionDenylistJobDelivered 标记 sid 已写入 gateway Redis denylist。
	MarkSessionDenylistJobDelivered(ctx context.Context, jobID int64, appCode string, sessionID string, reason string, appliedDenyUntilMs int64, nowMs int64, nextRehydrateAtMs int64) error
	// MarkSessionDenylistJobRetryable 释放失败 job 并安排下一次重试。
	MarkSessionDenylistJobRetryable(ctx context.Context, jobID int64, errorMessage string, nextRetryAtMs int64, nowMs int64) error
	// MaxSessionDenylistJobID 固化一次启动重水合的上界，避免并发新 job 让分页永不结束。
	MaxSessionDenylistJobID(ctx context.Context) (int64, error)
	// ListSessionDenylistJobsForHydration 忽略 status/lease 按稳定 cursor 读取未过期事实，启动恢复不依赖通用 claim。
	ListSessionDenylistJobsForHydration(ctx context.Context, nowMs int64, afterJobID int64, throughJobID int64, limit int) ([]authdomain.SessionDenylistJob, int64, error)
	// DeleteExpiredSessionDenylistJobs 只按持久化 deny_until 清理已经没有有效 access JWT 的事实。
	DeleteExpiredSessionDenylistJobs(ctx context.Context, nowMs int64, limit int) (int, error)
	// DeleteExpiredRefreshOutcomes 按 expires_at 索引清理过宽限期的加密 token pair。
	DeleteExpiredRefreshOutcomes(ctx context.Context, nowMs int64, limit int) (int, error)
	// UpdateSessionHeartbeat 刷新当前 App 登录会话心跳，必须校验 session 仍属于当前用户且未过期未吊销。
	UpdateSessionHeartbeat(ctx context.Context, sessionID string, userID int64, heartbeatAtMs int64, requestID string) (authdomain.Session, error)
	// FindThirdPartyIdentity 查找 provider + subject 的绑定。
	FindThirdPartyIdentity(ctx context.Context, provider string, providerSubject string) (authdomain.ThirdPartyIdentity, error)
	// CountUsersByRegisterDeviceID 统计当前 App 下已使用同一注册设备号的账号数量。
	CountUsersByRegisterDeviceID(ctx context.Context, appCode string, deviceID string) (int64, error)
	// ListDisplayUserIDsByRegisterDeviceID 按原注册顺序列出同设备已注册账号短号，用于上限判断和失败提示。
	ListDisplayUserIDsByRegisterDeviceID(ctx context.Context, appCode string, deviceID string) ([]string, error)
	// CreateThirdPartyUser 原子创建用户、默认短号、三方身份和首个 session。
	CreateThirdPartyUser(ctx context.Context, user userdomain.User, identity userdomain.Identity, thirdParty authdomain.ThirdPartyIdentity, session authdomain.Session, invite invitedomain.BindCommand, maxAccountsPerDevice int32) error
	// CompleteThirdPartyRegistration 原子补齐旧三方 pending 用户资料并创建首个注册完成 session。
	CompleteThirdPartyRegistration(ctx context.Context, command userdomain.CompleteOnboardingCommand, session authdomain.Session) (userdomain.User, error)
	// GetRegisterRiskConfig 读取当前 App 的注册风控配置。
	GetRegisterRiskConfig(ctx context.Context, appCode string) (authdomain.RegisterRiskConfig, error)
	// UpsertRegisterRiskConfig 保存当前 App 的注册风控配置。
	UpsertRegisterRiskConfig(ctx context.Context, config authdomain.RegisterRiskConfig) (authdomain.RegisterRiskConfig, error)
	// RecordLoginAudit 记录登录审计，主链路不会因为审计失败而失败。
	RecordLoginAudit(ctx context.Context, audit authdomain.LoginAudit) error
	// UpsertUserDevice 以“非空覆盖”语义合并设备档案；认证主链路不会因为它失败而失败。
	UpsertUserDevice(ctx context.Context, device authdomain.UserDevice) error
	// CreateLoginIPRiskJob 创建登录后异步 IP 风控任务，失败只影响复核能力。
	CreateLoginIPRiskJob(ctx context.Context, job authdomain.LoginIPRiskJob) error
	// ClaimLoginIPRiskJobs 按 DB 租约认领待处理风控任务。
	ClaimLoginIPRiskJobs(ctx context.Context, workerID string, nowMs int64, lockTTL time.Duration, batchSize int) ([]authdomain.LoginIPRiskJob, error)
	// CompleteLoginIPRiskJob 写入任务终态。
	CompleteLoginIPRiskJob(ctx context.Context, job authdomain.LoginIPRiskJob) error
	// CreateLoginIPRiskDecision 持久化 IP 风控决策审计。
	CreateLoginIPRiskDecision(ctx context.Context, decision authdomain.LoginIPRiskDecision) error
	// ListEnabledLoginRiskCountryBlocks 返回当前 App 的动态屏蔽国家词表。
	ListEnabledLoginRiskCountryBlocks(ctx context.Context) ([]authdomain.LoginRiskCountryBlock, error)
	// ListEnabledLoginRiskIPWhitelist 返回当前 App 的登录地区屏蔽 IP 白名单。
	ListEnabledLoginRiskIPWhitelist(ctx context.Context) ([]authdomain.LoginRiskIPWhitelist, error)
	// ListEnabledLoginRiskUserWhitelist 返回当前 App 的登录地区屏蔽用户白名单。
	ListEnabledLoginRiskUserWhitelist(ctx context.Context) ([]authdomain.LoginRiskUserWhitelist, error)
	// MarkLoginAuditBlocked 把登录成功后被异步风控撤销的审计行更新成阻断态。
	MarkLoginAuditBlocked(ctx context.Context, requestID string, userID int64, countryCode string, blockReason string) error
}

// UserRepository 为 auth service 提供登录签发前的用户快照读取。
// Auth service 不关心用户创建和短号修改细节，只需要判断用户是否可登录以及签发当前展示号。
type UserRepository interface {
	// GetUser 返回登录签发前需要的用户快照。
	GetUser(ctx context.Context, userID int64) (userdomain.User, error)
}

// IdentityRepository 为 auth service 提供靓号懒过期能力。
// 过期恢复仍由 identity repository 的事务完成，避免 auth service 手动改 users 和短号表。
type IdentityRepository interface {
	// ExpirePrettyDisplayUserID 原子恢复过期靓号，避免 token 签发过期展示号。
	ExpirePrettyDisplayUserID(ctx context.Context, command userdomain.ExpirePrettyDisplayUserIDCommand) (userdomain.Identity, error)
}

// ThirdPartyVerifier 把 provider credential 校验成稳定 provider_subject。
// gateway 不接触 provider secret，三方校验必须停留在 user-service。
type ThirdPartyVerifier interface {
	// Verify 校验 provider credential，并返回 provider 内稳定身份。
	Verify(ctx context.Context, provider string, credential string) (authdomain.ThirdPartyProfile, error)
}

// RegistrationRewardCommand 是三方首次注册成功后发往 activity-service 的幂等奖励命令。
type RegistrationRewardCommand struct {
	AppCode   string
	RequestID string
	UserID    int64
	CommandID string
}

// RegistrationRewardIssuer 隔离 auth service 和 activity protobuf，避免认证领域直接依赖外部 RPC 细节。
type RegistrationRewardIssuer interface {
	IssueRegistrationReward(ctx context.Context, command RegistrationRewardCommand) error
}

// RegistrationLevelBadgeCommand 是三方首次注册成功后发往 activity-service 的等级徽章发放命令。
type RegistrationLevelBadgeCommand struct {
	AppCode   string
	RequestID string
	UserID    int64
	CommandID string
}

// RegistrationLevelBadgeIssuer 隔离注册等级徽章发放 RPC；失败不能回滚用户注册主事务。
type RegistrationLevelBadgeIssuer interface {
	IssueRegistrationLevelBadges(ctx context.Context, command RegistrationLevelBadgeCommand) error
}

// IMAccountImporter 把 user-service 用户导入腾讯 IM 账号体系。
type IMAccountImporter interface {
	ImportAccount(ctx context.Context, userID int64, nickname string, faceURL string) error
}

// Config 保存认证底座需要的稳定参数。
type Config struct {
	// Issuer 写入 access token 的 iss claim。
	Issuer string
	// AccessTokenTTLSec 是 App 登录 access token 有效期。
	AccessTokenTTLSec int64
	// RefreshTokenTTLSec 是服务端 refresh session 有效期。
	RefreshTokenTTLSec int64
	// RefreshRotationGraceSec 允许直接上一代 refresh token 在响应丢失或并发时复用首次结果的秒数。
	RefreshRotationGraceSec int64
	// SigningAlg 当前只支持 HS256。
	SigningAlg string
	// SigningSecret 是 HS256 签名密钥。
	SigningSecret string
}

// RefreshMetricRecorder 是 refresh rotation 四个低基数指标的最小记录边界。
// 默认实现输出稳定结构化事件，现有日志采集可直接按 metric + app_code 聚合计数。
type RefreshMetricRecorder interface {
	Increment(ctx context.Context, metric string, appCode string)
}

type logRefreshMetricRecorder struct{}

func (logRefreshMetricRecorder) Increment(ctx context.Context, metric string, appCode string) {
	logx.Info(ctx, "auth_refresh_metric", slog.String("metric", metric), slog.String("app_code", appCode))
}

// LoginRiskPolicy 保存 IP 风控 worker 的判定和缓存策略。
type LoginRiskPolicy struct {
	Enabled              bool
	UnsupportedCountries []string
	BlockedTTL           time.Duration
	AllowedTTL           time.Duration
	UnknownTTL           time.Duration
	ProviderTimeout      time.Duration
	ProviderNames        []string
	DenylistTTL          time.Duration
}

// IPDecisionCache 是 Redis 风控快查缓存和 session denylist 的最小接口。
type IPDecisionCache interface {
	GetIPDecision(ctx context.Context, appCode string, clientIPHash string) (IPDecision, bool, error)
	SetIPDecision(ctx context.Context, appCode string, clientIPHash string, decision IPDecision, ttl time.Duration) error
	SetRevokedSession(ctx context.Context, appCode string, sessionID string, reason string, ttl time.Duration) error
	GetIPWhitelist(ctx context.Context, appCode string) ([]string, bool, error)
	SetIPWhitelist(ctx context.Context, appCode string, ips []string, ttl time.Duration) error
	GetUserWhitelist(ctx context.Context, appCode string) ([]int64, bool, error)
	SetUserWhitelist(ctx context.Context, appCode string, userIDs []int64, ttl time.Duration) error
}

// SessionDenylistEpochReader 暴露 Redis 进程 run_id；变化意味着重启/failover，需要从 MySQL 全量重水合未过期 job。
type SessionDenylistEpochReader interface {
	SessionDenylistEpoch(ctx context.Context) (string, error)
}

// RegisterRiskConfigCache 是注册风控配置的 Redis 快取边界。
// 该缓存只做读放大优化，MySQL 仍是配置的持久事实来源。
type RegisterRiskConfigCache interface {
	GetRegisterRiskConfig(ctx context.Context, appCode string) (authdomain.RegisterRiskConfig, bool, error)
	SetRegisterRiskConfig(ctx context.Context, config authdomain.RegisterRiskConfig, ttl time.Duration) error
}

// IPDecision 是 Redis 中 IP 决策 JSON 的领域投影。
type IPDecision struct {
	Decision    string `json:"decision"`
	Country     string `json:"country"`
	Source      string `json:"source"`
	CheckedAtMs int64  `json:"checked_at_ms"`
	ExpiresAtMs int64  `json:"expires_at_ms"`
}

// IPGeoProvider 表达一个按优先级调用的 IP Geo 来源。
type IPGeoProvider interface {
	Name() string
	ResolveCountry(ctx context.Context, job authdomain.LoginIPRiskJob) (IPGeoResult, error)
}

// IPGeoResult 是 provider 返回的标准化结果。
type IPGeoResult struct {
	CountryCode string
	Confidence  int
}

// Service 承载登录注册用例。
type Service struct {
	// cfg 保存 token 签发配置。
	cfg Config
	// authRepository 负责密码身份、session、三方绑定和登录审计。
	authRepository AuthRepository
	// userRepository 提供登录签发前的用户主状态读取。
	userRepository UserRepository
	// identityRepository 提供靓号懒过期恢复事务。
	identityRepository IdentityRepository
	// countryRegionRepository 提供三方首次注册时的国家主数据校验和区域解析。
	countryRegionRepository userservice.CountryRegionRepository
	// idGenerator 生成三方首次登录时的新 user_id。
	idGenerator userservice.IDGenerator
	// displayUserIDAllocator 生成三方首次登录时的默认短号候选。
	displayUserIDAllocator userservice.DisplayUserIDAllocator
	// thirdPartyVerifier 把 provider credential 校验成稳定 subject。
	thirdPartyVerifier ThirdPartyVerifier
	// now 是服务时钟，测试可注入固定时间。
	now func() time.Time
	// allocateMaxAttempts 是默认短号冲突重试上限。
	allocateMaxAttempts int
	// loginRiskPolicy 控制登录 IP 风控 worker 的判定和 TTL。
	loginRiskPolicy LoginRiskPolicy
	// ipDecisionCache 读写 Redis IP 决策和 session denylist，nil 时按 fail-open 处理。
	ipDecisionCache IPDecisionCache
	// registerRiskConfigCache 缓存注册风控配置，后台修改后会立即刷新同一个 Redis key。
	registerRiskConfigCache RegisterRiskConfigCache
	// ipGeoProviders 按顺序提供 IP 国家解析能力。
	ipGeoProviders []IPGeoProvider
	// registrationRewardIssuer 在注册事务提交后异步式触发奖励；失败只记录日志，不回滚用户注册。
	registrationRewardIssuer RegistrationRewardIssuer
	// registrationLevelBadgeIssuer 在注册事务提交后补发三条等级轨道的 1 级徽章。
	registrationLevelBadgeIssuer RegistrationLevelBadgeIssuer
	// imAccountImporter 在用户创建成功后补齐腾讯 IM 账号；UserSig 入口仍会兜底补偿历史账号。
	imAccountImporter IMAccountImporter
	// refreshMetrics 只记录固定指标名和 App 维度，避免 session/user 等高基数标签污染指标系统。
	refreshMetrics RefreshMetricRecorder
}

// Option 调整 auth service 的依赖和策略。
type Option func(*Service)

// New 创建认证服务。
func New(cfg Config, options ...Option) *Service {
	// 默认依赖只保证可构造，生产必须通过 app 层注入 repository 和真实配置。
	svc := &Service{
		cfg:                    cfg,
		idGenerator:            idgen.NewInt64Generator(0),
		displayUserIDAllocator: userservice.NumericDisplayUserIDAllocator{},
		thirdPartyVerifier:     NewStaticThirdPartyVerifier([]string{"wechat"}),
		now:                    time.Now,
		allocateMaxAttempts:    8,
		loginRiskPolicy:        DefaultLoginRiskPolicy(),
		refreshMetrics:         logRefreshMetricRecorder{},
	}

	for _, option := range options {
		// option 只替换依赖和策略，不执行 IO。
		option(svc)
	}

	return svc
}

// WithRefreshMetricRecorder 允许测试或后续 metrics exporter 注入计数器实现。
func WithRefreshMetricRecorder(recorder RefreshMetricRecorder) Option {
	return func(s *Service) {
		if recorder != nil {
			s.refreshMetrics = recorder
		}
	}
}

func (s *Service) SessionDenylistEpoch(ctx context.Context) (string, error) {
	reader, ok := s.ipDecisionCache.(SessionDenylistEpochReader)
	if !ok || reader == nil {
		return "", xerr.New(xerr.Unavailable, "session denylist Redis epoch is unavailable")
	}
	return reader.SessionDenylistEpoch(ctx)
}

func DefaultLoginRiskPolicy() LoginRiskPolicy {
	return LoginRiskPolicy{
		Enabled:              true,
		UnsupportedCountries: []string{"CN"},
		BlockedTTL:           30 * 24 * time.Hour,
		AllowedTTL:           7 * 24 * time.Hour,
		UnknownTTL:           30 * time.Minute,
		ProviderTimeout:      800 * time.Millisecond,
		DenylistTTL:          7*24*time.Hour + time.Minute,
	}
}

func WithLoginRiskPolicy(policy LoginRiskPolicy) Option {
	return func(s *Service) {
		defaults := DefaultLoginRiskPolicy()
		if policy.BlockedTTL <= 0 {
			policy.BlockedTTL = defaults.BlockedTTL
		}
		if policy.AllowedTTL <= 0 {
			policy.AllowedTTL = defaults.AllowedTTL
		}
		if policy.UnknownTTL <= 0 {
			policy.UnknownTTL = defaults.UnknownTTL
		}
		if policy.ProviderTimeout <= 0 {
			policy.ProviderTimeout = defaults.ProviderTimeout
		}
		if policy.DenylistTTL <= 0 {
			policy.DenylistTTL = defaults.DenylistTTL
			if s.cfg.AccessTokenTTLSec > 0 {
				policy.DenylistTTL = time.Duration(s.cfg.AccessTokenTTLSec+60) * time.Second
			}
		}
		s.loginRiskPolicy = policy
	}
}

func WithIPDecisionCache(cache IPDecisionCache) Option {
	return func(s *Service) {
		s.ipDecisionCache = cache
	}
}

func WithRegisterRiskConfigCache(cache RegisterRiskConfigCache) Option {
	return func(s *Service) {
		s.registerRiskConfigCache = cache
	}
}

func WithIPGeoProviders(providers []IPGeoProvider) Option {
	return func(s *Service) {
		s.ipGeoProviders = providers
	}
}

// WithRegistrationRewardIssuer 注入注册奖励发放 client；nil 表示本环境不启用该集成。
func WithRegistrationRewardIssuer(issuer RegistrationRewardIssuer) Option {
	return func(s *Service) {
		if issuer != nil {
			s.registrationRewardIssuer = issuer
		}
	}
}

// WithRegistrationLevelBadgeIssuer 注入注册等级徽章发放 client；nil 表示本环境不启用该集成。
func WithRegistrationLevelBadgeIssuer(issuer RegistrationLevelBadgeIssuer) Option {
	return func(s *Service) {
		if issuer != nil {
			s.registrationLevelBadgeIssuer = issuer
		}
	}
}

// WithIMAccountImporter 注入腾讯 IM 账号导入边界；nil 表示当前环境不触发真实 IM 副作用。
func WithIMAccountImporter(importer IMAccountImporter) Option {
	return func(s *Service) {
		if importer != nil {
			s.imAccountImporter = importer
		}
	}
}

// WithAuthRepository 注入纯认证持久化实现。
func WithAuthRepository(repository AuthRepository) Option {
	return func(s *Service) {
		if repository != nil {
			// nil 不覆盖现有配置，避免测试误传导致 service 不可用。
			s.authRepository = repository
		}
	}
}

// WithUserRepository 注入用户快照读取实现。
func WithUserRepository(repository UserRepository) Option {
	return func(s *Service) {
		if repository != nil {
			// auth service 只读取用户快照，不直接创建或修改用户资料。
			s.userRepository = repository
		}
	}
}

// WithIdentityRepository 注入短号懒过期实现。
func WithIdentityRepository(repository IdentityRepository) Option {
	return func(s *Service) {
		if repository != nil {
			// identityRepository 只负责过期靓号恢复事务。
			s.identityRepository = repository
		}
	}
}

// WithCountryRegionRepository 注入国家和区域解析 repository。
func WithCountryRegionRepository(repository userservice.CountryRegionRepository) Option {
	return func(s *Service) {
		if repository != nil {
			s.countryRegionRepository = repository
		}
	}
}

// WithIDGenerator 注入 user_id 发号器。
func WithIDGenerator(generator userservice.IDGenerator) Option {
	return func(s *Service) {
		if generator != nil {
			// 三方首次登录需要生成新用户 ID。
			s.idGenerator = generator
		}
	}
}

// WithDisplayUserIDAllocator 注入短号候选生成器。
func WithDisplayUserIDAllocator(allocator userservice.DisplayUserIDAllocator) Option {
	return func(s *Service) {
		if allocator != nil {
			// allocator 只生成候选，唯一性由 repository 事务保证。
			s.displayUserIDAllocator = allocator
		}
	}
}

// WithThirdPartyVerifier 注入三方 provider 校验器。
func WithThirdPartyVerifier(verifier ThirdPartyVerifier) Option {
	return func(s *Service) {
		if verifier != nil {
			// verifier 可替换为真实三方 SDK 实现。
			s.thirdPartyVerifier = verifier
		}
	}
}

// WithClock 注入时间函数，保证 session 轮换测试稳定。
func WithClock(now func() time.Time) Option {
	return func(s *Service) {
		if now != nil {
			// 固定时钟用于 session 过期和 refresh 轮换测试。
			s.now = now
		}
	}
}

// WithDisplayUserIDAllocateMaxAttempts 配置默认短号分配重试次数。
func WithDisplayUserIDAllocateMaxAttempts(maxAttempts int) Option {
	return func(s *Service) {
		if maxAttempts > 0 {
			// 只接受正数，避免三方注册永远无法分配默认短号。
			s.allocateMaxAttempts = maxAttempts
		}
	}
}
