package auth

import (
	"context"
	"fmt"
	"log/slog"
	"strings"
	"time"
	_ "time/tzdata"
	"unicode/utf8"

	"hyapp/pkg/appcode"
	"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"
)

const (
	// 以下长度必须和 services/user-service/deploy/mysql/initdb/001_user_service.sql 的 users 表 VARCHAR 保持一致。
	maxRegistrationUsername       = userdomain.ProfileUsernameMaxRunes
	maxRegistrationGender         = 32
	maxRegistrationCountry        = 64
	maxRegistrationInviteCode     = 64
	maxRegistrationIP             = 64
	maxRegistrationUserAgent      = 512
	maxRegistrationCountryByIP    = 64
	maxRegistrationDeviceID       = 128
	maxRegistrationDevice         = 128
	maxRegistrationOSVersion      = 64
	maxRegistrationAvatar         = userdomain.ProfileAvatarMaxRunes
	maxRegistrationAppVersion     = 64
	maxRegistrationBuildNumber    = 64
	maxRegistrationSource         = 64
	maxRegistrationInstallChannel = 64
	maxRegistrationCampaign       = 128
	maxRegistrationPlatform       = 16
	maxRegistrationLanguage       = 32
	maxRegistrationTimezone       = 64
)

// LoginThirdParty 校验三方凭证，存在则登录，不存在则创建用户和三方绑定。
func (s *Service) LoginThirdParty(ctx context.Context, provider string, credential string, registration authdomain.ThirdPartyRegistration, meta Meta) (authdomain.Token, bool, error) {
	meta.AppCode = appcode.Normalize(meta.AppCode)
	registration.AppCode = meta.AppCode
	provider = strings.ToLower(strings.TrimSpace(provider))
	credential = strings.TrimSpace(credential)
	registration = normalizeThirdPartyRegistration(registration, meta)
	meta.Platform = registration.Platform
	meta.AppVersion = registration.AppVersion
	meta.BuildNumber = registration.BuildNumber
	meta.Language = registration.Language
	meta.Timezone = registration.Timezone
	if provider == "" || credential == "" {
		// provider/credential 是认证材料，缺失时属于请求参数错误，不进入 provider verifier。
		return authdomain.Token{}, false, xerr.New(xerr.InvalidArgument, "provider and credential are required")
	}
	if registration.DeviceID == "" || registration.Platform == "" {
		// device_id 绑定 refresh session，platform 是注册来源必填快照；两者缺失都不能创建用户主记录。
		return authdomain.Token{}, false, xerr.New(xerr.InvalidArgument, "device_id and platform are required")
	}
	if err := validateThirdPartyRegistration(registration); err != nil {
		return authdomain.Token{}, false, err
	}
	if s.authRepository == nil {
		return authdomain.Token{}, false, xerr.New(xerr.Unavailable, "auth repository is not configured")
	}

	// provider credential 只在 user-service 内校验，gateway 不接触 provider secret。
	profile, err := s.thirdPartyVerifier.Verify(ctx, provider, credential)
	if err != nil {
		s.audit(ctx, meta, 0, loginThird, provider, resultFailed, string(xerr.AuthFailed))
		return authdomain.Token{}, false, authFailed()
	}

	// 已存在 provider + subject 绑定时走登录路径，否则走注册事务。
	identity, err := s.authRepository.FindThirdPartyIdentity(ctx, provider, profile.ProviderSubject)
	if err == nil {
		return s.loginExistingThirdParty(ctx, identity, registration, meta)
	}
	if !xerr.IsCode(err, xerr.NotFound) {
		// 非 not found 的存储错误不能退化为注册。
		logx.Error(logx.With(ctx, slog.String("request_id", meta.RequestID), slog.String("app_code", meta.AppCode)), "third_party_lookup_failed", err, slog.String("component", "user_auth"), slog.String("provider", provider))
		s.audit(ctx, meta, 0, loginThird, provider, resultFailed, string(xerr.CodeOf(err)))
		return authdomain.Token{}, false, err
	}
	maxAccountsPerDevice, err := s.ensureDeviceCanRegisterThirdParty(ctx, registration, meta, provider)
	if err != nil {
		return authdomain.Token{}, false, err
	}
	registration, err = s.resolveRegistrationCountry(ctx, registration)
	if err != nil {
		return authdomain.Token{}, false, err
	}

	return s.createThirdPartyUser(ctx, provider, profile, registration, meta, maxAccountsPerDevice)
}

// RegisterThirdParty 校验三方凭证并一次性完成 App 自然注册。
// 该入口服务 Flutter 资料页完成动作：资料未齐不建用户，不向客户端暴露 profile_required session。
func (s *Service) RegisterThirdParty(ctx context.Context, provider string, credential string, registration authdomain.ThirdPartyRegistration, meta Meta) (authdomain.Token, bool, error) {
	meta.AppCode = appcode.Normalize(meta.AppCode)
	registration.AppCode = meta.AppCode
	provider = strings.ToLower(strings.TrimSpace(provider))
	credential = strings.TrimSpace(credential)
	registration = normalizeThirdPartyRegistration(registration, meta)
	meta.Platform = registration.Platform
	meta.AppVersion = registration.AppVersion
	meta.BuildNumber = registration.BuildNumber
	meta.Language = registration.Language
	meta.Timezone = registration.Timezone
	if provider == "" || credential == "" {
		// provider/credential 缺失时不能进入 verifier，避免把客户端参数错误伪装成三方认证失败。
		return authdomain.Token{}, false, xerr.New(xerr.InvalidArgument, "provider and credential are required")
	}
	if registration.DeviceID == "" || registration.Platform == "" {
		// 注册完成后会立即创建 refresh session；缺设备或平台会让 session 和注册来源失去可追踪性。
		return authdomain.Token{}, false, xerr.New(xerr.InvalidArgument, "device_id and platform are required")
	}
	if err := validateThirdPartyRegistrationRequiredForCreate(registration); err != nil {
		// 新注册入口承诺不落 pending 用户，因此 username/avatar/gender/country 必须在事务前全部具备。
		return authdomain.Token{}, false, err
	}
	if err := validateThirdPartyRegistration(registration); err != nil {
		return authdomain.Token{}, false, err
	}
	if s.authRepository == nil {
		return authdomain.Token{}, false, xerr.New(xerr.Unavailable, "auth repository is not configured")
	}
	if !s.TokenConfigValid() {
		// 资料和三方绑定一旦提交就必须返回可用 token；签名配置不完整时提前失败，避免写出不可登录用户。
		return authdomain.Token{}, false, xerr.New(xerr.Unavailable, "token service is not configured")
	}

	profile, err := s.thirdPartyVerifier.Verify(ctx, provider, credential)
	if err != nil {
		s.audit(ctx, meta, 0, loginThird, provider, resultFailed, string(xerr.AuthFailed))
		return authdomain.Token{}, false, authFailed()
	}

	identity, err := s.authRepository.FindThirdPartyIdentity(ctx, provider, profile.ProviderSubject)
	if err == nil {
		// 兼容旧 Flutter 流程已创建的 profile_required 占位用户；同一 provider subject 不能新建第二个账号。
		return s.registerExistingThirdParty(ctx, identity, registration, meta)
	}
	if !xerr.IsCode(err, xerr.NotFound) {
		logx.Error(logx.With(ctx, slog.String("request_id", meta.RequestID), slog.String("app_code", meta.AppCode)), "third_party_register_lookup_failed", err, slog.String("component", "user_auth"), slog.String("provider", provider))
		s.audit(ctx, meta, 0, loginThird, provider, resultFailed, string(xerr.CodeOf(err)))
		return authdomain.Token{}, false, err
	}

	maxAccountsPerDevice, err := s.ensureDeviceCanRegisterThirdParty(ctx, registration, meta, provider)
	if err != nil {
		return authdomain.Token{}, false, err
	}
	registration, err = s.resolveRegistrationCountry(ctx, registration)
	if err != nil {
		return authdomain.Token{}, false, err
	}

	return s.createRegisteredThirdPartyUser(ctx, provider, profile, registration, meta, maxAccountsPerDevice)
}

// CheckThirdPartyRegistered 只校验 provider credential，并判断当前 App 下是否已有完整注册用户。
// 它不创建用户、不创建 session，专门给 Flutter 决定“进首页登录链路”还是“进资料页注册链路”。
func (s *Service) CheckThirdPartyRegistered(ctx context.Context, provider string, credential string, meta Meta) (bool, error) {
	meta.AppCode = appcode.Normalize(meta.AppCode)
	provider = strings.ToLower(strings.TrimSpace(provider))
	credential = strings.TrimSpace(credential)
	if provider == "" || credential == "" {
		// 缺认证材料是客户端参数错误；不能返回 registered=false 掩盖真实调用错误。
		return false, xerr.New(xerr.InvalidArgument, "provider and credential are required")
	}
	if s.authRepository == nil {
		return false, xerr.New(xerr.Unavailable, "auth repository is not configured")
	}

	profile, err := s.thirdPartyVerifier.Verify(ctx, provider, credential)
	if err != nil {
		return false, authFailed()
	}
	identity, err := s.authRepository.FindThirdPartyIdentity(ctx, provider, profile.ProviderSubject)
	if xerr.IsCode(err, xerr.NotFound) {
		// 未绑定表示从注册视角还不能进首页；客户端应继续资料页和注册接口。
		return false, nil
	}
	if err != nil {
		return false, err
	}

	user, err := s.freshUser(ctx, identity.UserID, s.now().UnixMilli(), meta.RequestID)
	if xerr.IsCode(err, xerr.NotFound) {
		// 极少数脏数据只有三方绑定没有用户；按未完成注册处理，避免前端误进首页。
		return false, nil
	}
	if err != nil {
		return false, err
	}
	return user.ProfileCompleted, nil
}

func (s *Service) ensureDeviceCanRegisterThirdParty(ctx context.Context, registration authdomain.ThirdPartyRegistration, meta Meta, provider string) (int32, error) {
	// 设备号限制只约束“创建新账号”路径；已绑定三方身份登录不经过这里，避免把正常跨设备登录误拦。
	config, err := s.GetRegisterRiskConfig(ctx)
	if err != nil {
		s.audit(ctx, meta, 0, loginThird, provider, resultFailed, string(xerr.CodeOf(err)))
		return 0, err
	}
	count, err := s.authRepository.CountUsersByRegisterDeviceID(ctx, registration.AppCode, registration.DeviceID)
	if err != nil {
		// 设备查重失败不能退化为注册，否则并发和依赖异常都会突破一机一号约束。
		s.audit(ctx, meta, 0, loginThird, provider, resultFailed, string(xerr.CodeOf(err)))
		return 0, err
	}
	if count < int64(config.MaxAccountsPerDevice) {
		return config.MaxAccountsPerDevice, nil
	}

	// 已达到配置上限时拒绝创建新账号；审计不写已有 user_id，避免客户端枚举设备归属。
	s.audit(ctx, meta, 0, loginThird, provider, resultFailed, string(xerr.DeviceAlreadyRegistered))
	return 0, xerr.New(xerr.DeviceAlreadyRegistered, "device already registered")
}

func (s *Service) loginExistingThirdParty(ctx context.Context, identity authdomain.ThirdPartyIdentity, registration authdomain.ThirdPartyRegistration, meta Meta) (authdomain.Token, bool, error) {
	// 已绑定三方用户登录时仍要重新读取用户快照，确保禁用状态和靓号过期生效。
	user, err := s.freshUser(ctx, identity.UserID, s.now().UnixMilli(), meta.RequestID)
	if err != nil {
		s.audit(ctx, meta, identity.UserID, loginThird, identity.Provider, resultFailed, string(xerr.CodeOf(err)))
		return authdomain.Token{}, false, err
	}
	if !user.CanLogin() {
		// 禁用用户不能通过三方登录绕过状态限制。
		s.audit(ctx, meta, user.UserID, loginThird, identity.Provider, resultFailed, string(xerr.UserDisabled))
		return authdomain.Token{}, false, xerr.New(xerr.UserDisabled, "user is disabled")
	}

	// 每次三方登录创建新的 refresh session。
	session, refreshToken, err := s.newSession(meta.AppCode, user.UserID, registration.DeviceID)
	if err != nil {
		return authdomain.Token{}, false, err
	}
	if err := s.authRepository.CreateSession(ctx, session); err != nil {
		return authdomain.Token{}, false, err
	}

	s.audit(ctx, meta, user.UserID, loginThird, identity.Provider, resultSuccess, "")
	// 老用户换机三方登录也要立即出现在设备 tab；header 缺失时回退 body 设备字段。
	s.recordUserDevice(ctx, deviceMetaWithRegistration(meta, registration), user.UserID, registration.DeviceID)
	// isNewUser=false 表示本次只创建 session，没有创建用户。
	token, err := s.issueToken(user, session.SessionID, session.DeviceID, refreshToken, session.ExpiresAtMs)
	if err != nil {
		return authdomain.Token{}, false, err
	}
	s.enqueueLoginIPRiskJob(ctx, meta, token, normalizeThirdPartyChannel(identity.Provider), registration.Platform, registration.Language, registration.Timezone)
	return token, false, nil
}

func (s *Service) registerExistingThirdParty(ctx context.Context, identity authdomain.ThirdPartyIdentity, registration authdomain.ThirdPartyRegistration, meta Meta) (authdomain.Token, bool, error) {
	// 已完成资料的同一三方身份按登录处理，保持客户端重试和多端登录的幂等语义。
	user, err := s.freshUser(ctx, identity.UserID, s.now().UnixMilli(), meta.RequestID)
	if err != nil {
		s.audit(ctx, meta, identity.UserID, loginThird, identity.Provider, resultFailed, string(xerr.CodeOf(err)))
		return authdomain.Token{}, false, err
	}
	if !user.CanLogin() {
		s.audit(ctx, meta, user.UserID, loginThird, identity.Provider, resultFailed, string(xerr.UserDisabled))
		return authdomain.Token{}, false, xerr.New(xerr.UserDisabled, "user is disabled")
	}
	if user.ProfileCompleted {
		return s.loginExistingThirdParty(ctx, identity, registration, meta)
	}

	registration, err = s.resolveRegistrationCountry(ctx, registration)
	if err != nil {
		s.audit(ctx, meta, identity.UserID, loginThird, identity.Provider, resultFailed, string(xerr.CodeOf(err)))
		return authdomain.Token{}, false, err
	}
	session, refreshToken, err := s.newSession(meta.AppCode, identity.UserID, registration.DeviceID)
	if err != nil {
		return authdomain.Token{}, false, err
	}
	updated, err := s.authRepository.CompleteThirdPartyRegistration(ctx, userdomain.CompleteOnboardingCommand{
		AppCode:       meta.AppCode,
		UserID:        identity.UserID,
		Username:      registration.Username,
		Avatar:        registration.Avatar,
		Gender:        registration.Gender,
		Country:       registration.Country,
		RegionID:      registration.RegionID,
		InviteCode:    registration.InviteCode,
		RequestID:     meta.RequestID,
		CompletedAtMs: s.now().UnixMilli(),
	}, session)
	if err != nil {
		s.audit(ctx, meta, identity.UserID, loginThird, identity.Provider, resultFailed, string(xerr.CodeOf(err)))
		return authdomain.Token{}, false, err
	}

	s.audit(ctx, meta, updated.UserID, loginThird, identity.Provider, resultSuccess, "")
	// 补齐资料完成注册同样属于认证成功，设备档案与 session 创建保持同步。
	s.recordUserDevice(ctx, deviceMetaWithRegistration(meta, registration), updated.UserID, registration.DeviceID)
	token, err := s.issueToken(updated, session.SessionID, session.DeviceID, refreshToken, session.ExpiresAtMs)
	if err != nil {
		return authdomain.Token{}, false, err
	}
	s.importIMAccountAfterCommit(ctx, updated, meta)
	s.enqueueLoginIPRiskJob(ctx, meta, token, normalizeThirdPartyChannel(identity.Provider), registration.Platform, registration.Language, registration.Timezone)
	s.issueRegistrationRewardAfterCommit(ctx, registration.AppCode, updated.UserID, meta)
	s.issueRegistrationLevelBadgesAfterCommit(ctx, registration.AppCode, updated.UserID, meta)
	return token, false, nil
}

func (s *Service) createThirdPartyUser(ctx context.Context, provider string, profile authdomain.ThirdPartyProfile, registration authdomain.ThirdPartyRegistration, meta Meta, maxAccountsPerDevice int32) (authdomain.Token, bool, error) {
	var lastErr error
	for attempt := 0; attempt < s.allocateMaxAttempts; attempt++ {
		// 三方首次登录要同时生成 user_id 和默认短号，短号冲突时有限重试。
		user, displayIdentity := s.newUserWithIdentity(attempt)
		user = applyThirdPartyRegistration(user, registration)
		displayUserID, err := s.authRepository.AllocateDisplayUserID(ctx, registration.AppCode)
		if err != nil {
			return authdomain.Token{}, false, err
		}
		user.DefaultDisplayUserID = displayUserID
		user.CurrentDisplayUserID = displayUserID
		displayIdentity.DisplayUserID = displayUserID
		displayIdentity.DefaultDisplayUserID = displayUserID
		displayIdentity.AppCode = registration.AppCode
		if !userdomain.ValidDisplayUserID(displayIdentity.DisplayUserID) {
			// 非法候选直接跳过，避免进入 repository 唯一性判断。
			continue
		}
		session, refreshToken, err := s.newSession(registration.AppCode, user.UserID, registration.DeviceID)
		if err != nil {
			return authdomain.Token{}, false, err
		}

		thirdParty := authdomain.ThirdPartyIdentity{
			AppCode:         registration.AppCode,
			UserID:          user.UserID,
			Provider:        provider,
			ProviderSubject: profile.ProviderSubject,
			ProviderUnionID: profile.ProviderUnionID,
			CreatedAtMs:     user.CreatedAtMs,
			UpdatedAtMs:     user.UpdatedAtMs,
		}

		inviteBind := invitedomain.BindCommand{
			AppCode:         registration.AppCode,
			InvitedUserID:   user.UserID,
			InvitedRegionID: registration.RegionID,
			InviteCode:      registration.InviteCode,
			Source:          "third_party_registration",
			RequestID:       meta.RequestID,
			BoundAtMs:       user.CreatedAtMs,
		}
		err = s.authRepository.CreateThirdPartyUser(ctx, user, displayIdentity, thirdParty, session, inviteBind, maxAccountsPerDevice)
		if err == nil {
			// repository 事务成功后，用户、默认短号、三方绑定和首个 session 同时存在。
			s.audit(ctx, meta, user.UserID, loginThird, provider, resultSuccess, "")
			// 新注册用户的首个设备档案与注册事务同请求写入，后台无需等下次登录。
			s.recordUserDevice(ctx, deviceMetaWithRegistration(meta, registration), user.UserID, registration.DeviceID)
			token, tokenErr := s.issueToken(user, session.SessionID, session.DeviceID, refreshToken, session.ExpiresAtMs)
			if tokenErr != nil {
				return authdomain.Token{}, false, tokenErr
			}
			s.importIMAccountAfterCommit(ctx, user, meta)
			s.enqueueLoginIPRiskJob(ctx, meta, token, normalizeThirdPartyChannel(provider), registration.Platform, registration.Language, registration.Timezone)
			s.issueRegistrationRewardAfterCommit(ctx, registration.AppCode, user.UserID, meta)
			s.issueRegistrationLevelBadgesAfterCommit(ctx, registration.AppCode, user.UserID, meta)
			return token, true, nil
		}
		if xerr.IsCode(err, xerr.DisplayUserIDExists) {
			// 默认短号冲突可重试，user_id 和 display_user_id 都会重新生成。
			lastErr = err
			continue
		}
		if xerr.IsCode(err, xerr.Conflict) {
			// 并发注册同一 provider subject 时，尝试转为已有三方登录。
			identity, findErr := s.authRepository.FindThirdPartyIdentity(ctx, provider, profile.ProviderSubject)
			if findErr == nil {
				return s.loginExistingThirdParty(ctx, identity, registration, meta)
			}
		}

		s.audit(ctx, meta, 0, loginThird, provider, resultFailed, string(xerr.CodeOf(err)))
		logx.Error(logx.With(ctx, slog.String("request_id", meta.RequestID), slog.String("app_code", meta.AppCode)), "third_party_register_failed", err, slog.String("component", "user_auth"), slog.String("provider", provider))
		return authdomain.Token{}, false, err
	}

	if lastErr != nil {
		// 明确记录是短号分配耗尽，而不是 provider credential 失败。
		s.audit(ctx, meta, 0, loginThird, provider, resultFailed, string(xerr.DisplayUserIDAllocateFailed))
	}
	return authdomain.Token{}, false, xerr.New(xerr.DisplayUserIDAllocateFailed, "display_user_id allocation failed")
}

func (s *Service) createRegisteredThirdPartyUser(ctx context.Context, provider string, profile authdomain.ThirdPartyProfile, registration authdomain.ThirdPartyRegistration, meta Meta, maxAccountsPerDevice int32) (authdomain.Token, bool, error) {
	var lastErr error
	for attempt := 0; attempt < s.allocateMaxAttempts; attempt++ {
		// 注册专用入口直接创建 completed 用户；所有公开资料、绑定、session 和注册事实必须同事务提交。
		user, displayIdentity := s.newUserWithIdentity(attempt)
		user = applyThirdPartyRegistration(user, registration)
		user.ProfileCompleted = true
		user.ProfileCompletedAtMs = user.CreatedAtMs
		user.OnboardingStatus = userdomain.OnboardingStatusCompleted
		displayUserID, err := s.authRepository.AllocateDisplayUserID(ctx, registration.AppCode)
		if err != nil {
			return authdomain.Token{}, false, err
		}
		user.DefaultDisplayUserID = displayUserID
		user.CurrentDisplayUserID = displayUserID
		displayIdentity.DisplayUserID = displayUserID
		displayIdentity.DefaultDisplayUserID = displayUserID
		displayIdentity.AppCode = registration.AppCode
		if !userdomain.ValidDisplayUserID(displayIdentity.DisplayUserID) {
			// 非法候选不写库，继续拿下一组 user_id/display_user_id，保持默认短号约束一致。
			continue
		}
		session, refreshToken, err := s.newSession(registration.AppCode, user.UserID, registration.DeviceID)
		if err != nil {
			return authdomain.Token{}, false, err
		}
		thirdParty := authdomain.ThirdPartyIdentity{
			AppCode:         registration.AppCode,
			UserID:          user.UserID,
			Provider:        provider,
			ProviderSubject: profile.ProviderSubject,
			ProviderUnionID: profile.ProviderUnionID,
			CreatedAtMs:     user.CreatedAtMs,
			UpdatedAtMs:     user.UpdatedAtMs,
		}
		inviteBind := invitedomain.BindCommand{
			AppCode:         registration.AppCode,
			InvitedUserID:   user.UserID,
			InvitedRegionID: registration.RegionID,
			InviteCode:      registration.InviteCode,
			Source:          "third_party_register",
			RequestID:       meta.RequestID,
			BoundAtMs:       user.CreatedAtMs,
		}
		err = s.authRepository.CreateThirdPartyUser(ctx, user, displayIdentity, thirdParty, session, inviteBind, maxAccountsPerDevice)
		if err == nil {
			s.audit(ctx, meta, user.UserID, loginThird, provider, resultSuccess, "")
			// 注册专用入口创建的 completed 用户同样落首个设备档案。
			s.recordUserDevice(ctx, deviceMetaWithRegistration(meta, registration), user.UserID, registration.DeviceID)
			token, tokenErr := s.issueToken(user, session.SessionID, session.DeviceID, refreshToken, session.ExpiresAtMs)
			if tokenErr != nil {
				return authdomain.Token{}, false, tokenErr
			}
			s.importIMAccountAfterCommit(ctx, user, meta)
			s.enqueueLoginIPRiskJob(ctx, meta, token, normalizeThirdPartyChannel(provider), registration.Platform, registration.Language, registration.Timezone)
			s.issueRegistrationRewardAfterCommit(ctx, registration.AppCode, user.UserID, meta)
			s.issueRegistrationLevelBadgesAfterCommit(ctx, registration.AppCode, user.UserID, meta)
			return token, true, nil
		}
		if xerr.IsCode(err, xerr.DisplayUserIDExists) {
			// 默认短号冲突可重试；同一次 Firebase subject 不会因为短号冲突暴露成注册失败。
			lastErr = err
			continue
		}
		if xerr.IsCode(err, xerr.Conflict) {
			identity, findErr := s.authRepository.FindThirdPartyIdentity(ctx, provider, profile.ProviderSubject)
			if findErr == nil {
				return s.registerExistingThirdParty(ctx, identity, registration, meta)
			}
		}

		s.audit(ctx, meta, 0, loginThird, provider, resultFailed, string(xerr.CodeOf(err)))
		logx.Error(logx.With(ctx, slog.String("request_id", meta.RequestID), slog.String("app_code", meta.AppCode)), "third_party_complete_register_failed", err, slog.String("component", "user_auth"), slog.String("provider", provider))
		return authdomain.Token{}, false, err
	}

	if lastErr != nil {
		s.audit(ctx, meta, 0, loginThird, provider, resultFailed, string(xerr.DisplayUserIDAllocateFailed))
	}
	return authdomain.Token{}, false, xerr.New(xerr.DisplayUserIDAllocateFailed, "display_user_id allocation failed")
}

func (s *Service) issueRegistrationRewardAfterCommit(ctx context.Context, appCode string, userID int64, meta Meta) {
	if s.registrationRewardIssuer == nil {
		return
	}
	commandID := fmt.Sprintf("registration_reward:%s:%d", appcode.Normalize(appCode), userID)
	if err := s.registrationRewardIssuer.IssueRegistrationReward(ctx, RegistrationRewardCommand{
		AppCode:   appcode.Normalize(appCode),
		RequestID: meta.RequestID,
		UserID:    userID,
		CommandID: commandID,
	}); err != nil {
		// 注册主事务已经提交，奖励是后置权益发放；失败只能记录并依赖同 command_id 的人工或重试修复，不能让用户注册回滚。
		logx.Warn(logx.With(ctx, slog.String("request_id", meta.RequestID), slog.String("app_code", appcode.Normalize(appCode))), "registration_reward_issue_failed",
			slog.String("component", "user_auth"),
			slog.Int64("user_id", userID),
			slog.String("command_id", commandID),
			slog.String("error", err.Error()),
		)
	}
}

func (s *Service) issueRegistrationLevelBadgesAfterCommit(ctx context.Context, appCode string, userID int64, meta Meta) {
	if s.registrationLevelBadgeIssuer == nil {
		return
	}
	commandID := fmt.Sprintf("registration_level_badges:%s:%d", appcode.Normalize(appCode), userID)
	if err := s.registrationLevelBadgeIssuer.IssueRegistrationLevelBadges(ctx, RegistrationLevelBadgeCommand{
		AppCode:   appcode.Normalize(appCode),
		RequestID: meta.RequestID,
		UserID:    userID,
		CommandID: commandID,
	}); err != nil {
		// 等级徽章是注册后置权益；用户注册事务已经提交，只能依赖同 command_id 重试或补发修复。
		logx.Warn(logx.With(ctx, slog.String("request_id", meta.RequestID), slog.String("app_code", appcode.Normalize(appCode))), "registration_level_badges_issue_failed",
			slog.String("component", "user_auth"),
			slog.Int64("user_id", userID),
			slog.String("command_id", commandID),
			slog.String("error", err.Error()),
		)
	}
}

func normalizeThirdPartyRegistration(registration authdomain.ThirdPartyRegistration, meta Meta) authdomain.ThirdPartyRegistration {
	// 注册资料来自公网入口，进入领域前统一裁剪空白，避免数据库里出现不可见差异。
	registration.Username = strings.TrimSpace(registration.Username)
	registration.Gender = strings.TrimSpace(registration.Gender)
	registration.Country = strings.ToUpper(strings.TrimSpace(registration.Country))
	registration.InviteCode = invitedomain.NormalizeCode(registration.InviteCode)
	// 注册 IP 和 UA 只能来自 gateway 观察到的请求上下文，不能信任公网 JSON body。
	registration.IP = strings.TrimSpace(meta.ClientIP)
	registration.UserAgent = strings.TrimSpace(meta.UserAgent)
	registration.CountryByIP = normalizeCountryByIP(meta.CountryByIP)
	registration.DeviceID = strings.TrimSpace(registration.DeviceID)
	registration.Device = strings.TrimSpace(registration.Device)
	registration.OSVersion = strings.TrimSpace(registration.OSVersion)
	registration.Avatar = strings.TrimSpace(registration.Avatar)
	registration.BirthDate = strings.TrimSpace(registration.BirthDate)
	registration.AppVersion = strings.TrimSpace(registration.AppVersion)
	registration.BuildNumber = strings.TrimSpace(registration.BuildNumber)
	registration.Source = strings.TrimSpace(registration.Source)
	registration.InstallChannel = strings.TrimSpace(registration.InstallChannel)
	registration.Campaign = strings.TrimSpace(registration.Campaign)
	registration.Platform = strings.ToLower(strings.TrimSpace(registration.Platform))
	registration.Language = strings.TrimSpace(registration.Language)
	registration.Timezone = strings.TrimSpace(registration.Timezone)

	return registration
}

func validateThirdPartyRegistration(registration authdomain.ThirdPartyRegistration) error {
	if err := validateRegistrationLengths(registration); err != nil {
		return err
	}
	if registration.Country != "" && !userdomain.ValidCountryCode(registration.Country) {
		// 用户选择国家只接受 2 到 3 位大写英文国家码，是否可选由 countries 表决定。
		return xerr.New(xerr.InvalidArgument, "country must use 2 to 3 uppercase letters")
	}
	if registration.InviteCode != "" && !invitedomain.ValidCode(registration.InviteCode) {
		// 邀请码错误统一返回 INVALID_INVITE_CODE，避免客户端通过错误细节枚举邀请码。
		return xerr.New(xerr.InvalidInviteCode, "invalid invite code")
	}
	if registration.BirthDate != "" {
		if !userdomain.ValidProfileBirthDate(registration.BirthDate) {
			// 生日只保存日期，不接受带时区或非零填充格式，避免跨端解析不一致。
			return xerr.New(xerr.InvalidArgument, "birth must use yyyy-mm-dd")
		}
	}
	if registration.Avatar != "" && !userdomain.ValidProfileAvatarURL(registration.Avatar) {
		// avatar 进入可展示资料，不接受相对地址或非 HTTP(S) scheme。
		return xerr.New(xerr.InvalidArgument, "avatar must be an absolute http or https URL")
	}
	if registration.Platform != "android" && registration.Platform != "ios" {
		// 平台值进入统计和渠道分析，先收敛到明确枚举。
		return xerr.New(xerr.InvalidArgument, "platform must be android or ios")
	}
	if registration.Language != "" && !validLanguageTag(registration.Language) {
		// language 使用轻量 BCP 47 子集，覆盖 en-US、zh-Hant-CN 等常见客户端语言。
		return xerr.New(xerr.InvalidArgument, "language must use a BCP 47 language tag")
	}
	if registration.Timezone != "" {
		if _, err := time.LoadLocation(registration.Timezone); err != nil {
			// timezone 使用 IANA 名称；time/tzdata 保证 Alpine 镜像内也能稳定校验。
			return xerr.New(xerr.InvalidArgument, "timezone must use an IANA timezone name")
		}
	}

	return nil
}

func validateThirdPartyRegistrationRequiredForCreate(registration authdomain.ThirdPartyRegistration) error {
	if registration.Username == "" || registration.Country == "" || registration.Gender == "" || registration.Avatar == "" {
		// 注册即落用户主数据；最小公开资料不完整时拒绝创建，避免后台出现空昵称、空头像或 GLOBAL 国家用户。
		return xerr.New(xerr.InvalidArgument, "username, country, gender and avatar are required")
	}

	return nil
}

func validateRegistrationLengths(registration authdomain.ThirdPartyRegistration) error {
	checks := []struct {
		name  string
		value string
		max   int
	}{
		{name: "username", value: registration.Username, max: maxRegistrationUsername},
		{name: "gender", value: registration.Gender, max: maxRegistrationGender},
		{name: "country", value: registration.Country, max: maxRegistrationCountry},
		{name: "invite_code", value: registration.InviteCode, max: maxRegistrationInviteCode},
		{name: "register_ip", value: registration.IP, max: maxRegistrationIP},
		{name: "register_user_agent", value: registration.UserAgent, max: maxRegistrationUserAgent},
		{name: "country_by_ip", value: registration.CountryByIP, max: maxRegistrationCountryByIP},
		{name: "device_id", value: registration.DeviceID, max: maxRegistrationDeviceID},
		{name: "device", value: registration.Device, max: maxRegistrationDevice},
		{name: "os_version", value: registration.OSVersion, max: maxRegistrationOSVersion},
		{name: "avatar", value: registration.Avatar, max: maxRegistrationAvatar},
		{name: "app_version", value: registration.AppVersion, max: maxRegistrationAppVersion},
		{name: "build_number", value: registration.BuildNumber, max: maxRegistrationBuildNumber},
		{name: "source", value: registration.Source, max: maxRegistrationSource},
		{name: "install_channel", value: registration.InstallChannel, max: maxRegistrationInstallChannel},
		{name: "campaign", value: registration.Campaign, max: maxRegistrationCampaign},
		{name: "platform", value: registration.Platform, max: maxRegistrationPlatform},
		{name: "language", value: registration.Language, max: maxRegistrationLanguage},
		{name: "timezone", value: registration.Timezone, max: maxRegistrationTimezone},
	}
	for _, check := range checks {
		if utf8.RuneCountInString(check.value) > check.max {
			// MySQL VARCHAR 以字符数限制，使用 RuneCount 避免中文昵称被按 UTF-8 字节数误拒。
			return xerr.New(xerr.InvalidArgument, check.name+" is too long")
		}
	}

	return nil
}

func (s *Service) resolveRegistrationCountry(ctx context.Context, registration authdomain.ThirdPartyRegistration) (authdomain.ThirdPartyRegistration, error) {
	if registration.Country == "" && registration.CountryByIP == "" {
		// 没有国家输入时不要求国家主数据依赖，允许极简三方登录测试和匿名地域注册。
		return registration, nil
	}
	if s.countryRegionRepository == nil {
		// 只要需要写 country 或 country_by_ip，就必须经过国家主数据校验。
		return registration, xerr.New(xerr.Unavailable, "country repository is not configured")
	}

	if registration.Country != "" {
		country, ok, err := s.countryRegionRepository.ResolveEnabledCountryByCode(ctx, registration.Country)
		if err != nil {
			return registration, err
		}
		if !ok {
			return registration, xerr.New(xerr.InvalidArgument, "country is not supported")
		}
		registration.Country = country.CountryCode

		region, ok, err := s.countryRegionRepository.ResolveActiveRegionByCountry(ctx, country.CountryCode)
		if err != nil {
			return registration, err
		}
		if ok {
			// region_id 是服务端按国家映射计算出的快照，客户端不能直接提交。
			registration.RegionID = region.RegionID
		}
	}

	if registration.CountryByIP != "" {
		country, ok, err := s.countryRegionRepository.ResolveEnabledCountryByCode(ctx, registration.CountryByIP)
		if err != nil {
			return registration, err
		}
		if ok {
			// country_by_ip 只保留有效国家快照，不参与区域归属计算。
			registration.CountryByIP = country.CountryCode
		} else {
			registration.CountryByIP = ""
		}
	}

	return registration, nil
}

func normalizeCountryByIP(country string) string {
	// 边缘层常用 XX 表示未知国家；未知值不写入用户主表，避免误当真实归属。
	country = strings.ToUpper(strings.TrimSpace(country))
	if country == "XX" || country == "UNKNOWN" || !userdomain.ValidCountryCode(country) {
		return ""
	}

	return country
}

func validLanguageTag(language string) bool {
	parts := strings.Split(language, "-")
	if len(parts) == 0 || len(parts[0]) < 2 || len(parts[0]) > 8 {
		return false
	}
	for index, part := range parts {
		if part == "" || len(part) > 8 {
			return false
		}
		for _, char := range part {
			if !((char >= 'A' && char <= 'Z') || (char >= 'a' && char <= 'z') || (index > 0 && char >= '0' && char <= '9')) {
				return false
			}
		}
	}

	return true
}

func applyThirdPartyRegistration(user userdomain.User, registration authdomain.ThirdPartyRegistration) userdomain.User {
	// 用户资料和注册来源跟随 users 主记录一次性落库，三方 credential 不会进入用户表。
	user.Username = registration.Username
	user.AppCode = registration.AppCode
	user.Gender = registration.Gender
	user.Country = registration.Country
	user.RegionID = registration.RegionID
	user.InviteCode = registration.InviteCode
	user.RegisterIP = registration.IP
	user.RegisterUserAgent = registration.UserAgent
	user.CountryByIP = registration.CountryByIP
	user.RegisterDeviceID = registration.DeviceID
	user.RegisterDevice = registration.Device
	user.RegisterOSVersion = registration.OSVersion
	user.Avatar = registration.Avatar
	user.BirthDate = registration.BirthDate
	user.RegisterAppVersion = registration.AppVersion
	user.RegisterBuildNumber = registration.BuildNumber
	user.RegisterSource = registration.Source
	user.RegisterInstallChannel = registration.InstallChannel
	user.RegisterCampaign = registration.Campaign
	user.RegisterPlatform = registration.Platform
	user.Language = registration.Language
	user.Timezone = registration.Timezone
	return user
}

func (s *Service) newUserWithIdentity(attempt int) (userdomain.User, userdomain.Identity) {
	// user 和 identity 必须成对创建，避免出现没有默认短号的用户。
	userID := s.idGenerator.NewInt64()
	nowMs := s.now().UnixMilli()
	displayUserID := s.displayUserIDAllocator.NextDisplayUserID(userID, attempt)

	return userdomain.User{
			UserID:                   userID,
			DefaultDisplayUserID:     displayUserID,
			CurrentDisplayUserID:     displayUserID,
			CurrentDisplayUserIDKind: userdomain.DisplayUserIDKindDefault,
			ProfileCompleted:         false,
			ProfileCompletedAtMs:     0,
			OnboardingStatus:         userdomain.OnboardingStatusProfileRequired,
			Status:                   userdomain.StatusActive,
			CreatedAtMs:              nowMs,
			UpdatedAtMs:              nowMs,
		}, userdomain.Identity{
			UserID:               userID,
			DisplayUserID:        displayUserID,
			DefaultDisplayUserID: displayUserID,
			DisplayUserIDKind:    userdomain.DisplayUserIDKindDefault,
			Status:               userdomain.DisplayUserIDStatusActive,
		}
}
