package user

import (
	"context"
	"errors"
	"strings"

	"hyapp/pkg/appcode"
	"hyapp/pkg/xerr"
	hostdomain "hyapp/services/user-service/internal/domain/host"
	invitedomain "hyapp/services/user-service/internal/domain/invite"
	userdomain "hyapp/services/user-service/internal/domain/user"
)

// UpdateUserProfile 修改用户名、头像、性别和生日；权益制 App 的非空头像只能消费专用上传凭证。
func (s *Service) UpdateUserProfile(ctx context.Context, userID int64, username *string, avatar *string, gender *string, birth *string, avatarUploadID *string, requestID string) (userdomain.User, error) {
	if userID <= 0 {
		// user_id 必须来自 gateway 鉴权上下文，service 层仍兜底校验。
		return userdomain.User{}, xerr.New(xerr.InvalidArgument, "user_id is required")
	}
	if s.userRepository == nil {
		return userdomain.User{}, xerr.New(xerr.Unavailable, "user repository is not configured")
	}
	if err := s.requireCompletedProfile(ctx, userID); err != nil {
		// 注册页唯一入口是 CompleteOnboarding；未完成资料的用户不能通过资料编辑拼出半完成状态。
		return userdomain.User{}, err
	}
	username = trimmedStringPtr(username)
	avatar = trimmedStringPtr(avatar)
	avatarUploadID = trimmedStringPtr(avatarUploadID)
	gender = trimmedStringPtr(gender)
	birth = trimmedStringPtr(birth)
	if username == nil && avatar == nil && gender == nil && birth == nil && avatarUploadID == nil {
		// PATCH 语义要求至少有一个字段，否则客户端通常是传参错误。
		return userdomain.User{}, xerr.New(xerr.InvalidArgument, "profile update field is required")
	}
	if avatar != nil && avatarUploadID != nil {
		// URL 与凭证二选一，防止客户端提交合法凭证却用另一个任意 URL 覆盖其媒体事实。
		return userdomain.User{}, xerr.New(xerr.InvalidArgument, "avatar and avatar_upload_id cannot be used together")
	}
	resolvedUploadID := ""
	if avatarUploadID != nil {
		resolvedUploadID = strings.TrimSpace(*avatarUploadID)
		if resolvedUploadID == "" || s.avatarUploadRepository == nil {
			return userdomain.User{}, xerr.New(xerr.InvalidArgument, "avatar_upload_id is invalid")
		}
		registration, exists, err := s.avatarUploadRepository.GetAvatarUpload(ctx, userID, resolvedUploadID)
		if err != nil {
			return userdomain.User{}, err
		}
		if !exists || (registration.Status != avatarUploadStatusActive && registration.Status != "consumed") || strings.TrimSpace(registration.ObjectURL) == "" {
			return userdomain.User{}, xerr.New(xerr.Conflict, "avatar upload is not active")
		}
		resolvedAvatar := strings.TrimSpace(registration.ObjectURL)
		avatar = &resolvedAvatar
	}
	if username != nil && *username == "" {
		// 用户名是展示资料，禁止写成空字符串；头像和生日允许清空。
		return userdomain.User{}, xerr.New(xerr.InvalidArgument, "username is required")
	}
	if username != nil && userdomain.ProfileStringTooLong(*username, userdomain.ProfileUsernameMaxRunes) {
		// 提前按 users.username 的 VARCHAR 边界拒绝，避免把客户端错误暴露成 MySQL 错误。
		return userdomain.User{}, xerr.New(xerr.InvalidArgument, "username is too long")
	}
	if avatar != nil && *avatar != "" {
		if userdomain.ProfileStringTooLong(*avatar, userdomain.ProfileAvatarMaxRunes) {
			// 头像 URL 直接进入展示资料，长度错误必须稳定返回 INVALID_ARGUMENT。
			return userdomain.User{}, xerr.New(xerr.InvalidArgument, "avatar is too long")
		}
		if !userdomain.ValidProfileAvatarURL(*avatar) {
			// 非 HTTP(S) 或相对地址不应写入展示资料，避免客户端渲染和安全策略不一致。
			return userdomain.User{}, xerr.New(xerr.InvalidArgument, "avatar must be an absolute http or https URL")
		}
		if resolvedUploadID == "" {
			required, err := s.avatarUploadRequiredForRawURL(ctx, requestID, userID)
			if err != nil {
				return userdomain.User{}, err
			}
			if required {
				// 权益制 App 的静态和动态头像都先走同一上传入口，服务端才能从字节判断是否需要 VIP。
				return userdomain.User{}, xerr.New(xerr.InvalidArgument, "avatar_upload_id is required")
			}
		}
	}
	if gender != nil {
		if *gender == "" {
			return userdomain.User{}, xerr.New(xerr.InvalidArgument, "gender is required")
		}
		if userdomain.ProfileStringTooLong(*gender, userdomain.ProfileGenderMaxRunes) {
			return userdomain.User{}, xerr.New(xerr.InvalidArgument, "gender is too long")
		}
	}
	if birth != nil && *birth != "" {
		if !userdomain.ValidProfileBirthDate(*birth) {
			// 生日只保存日期，不接受时区或斜杠格式，避免客户端跨时区偏移。
			return userdomain.User{}, xerr.New(xerr.InvalidArgument, "birth must use yyyy-mm-dd")
		}
	}

	updated, err := s.userRepository.UpdateUserProfile(ctx, userdomain.ProfileUpdateCommand{
		AppCode:        appcode.FromContext(ctx),
		UserID:         userID,
		Username:       username,
		Avatar:         avatar,
		AvatarUploadID: resolvedUploadID,
		Gender:         gender,
		BirthDate:      birth,
		UpdatedAtMs:    s.now().UnixMilli(),
	})
	if errors.Is(err, userdomain.ErrAvatarUploadNotConsumable) {
		return userdomain.User{}, xerr.New(xerr.Conflict, "avatar upload is not consumable")
	}
	return updated, err
}

// UpdateUserProfileBackground 只修改个人信息页背景图；客户端传入已上传完成的图片 URL。
func (s *Service) UpdateUserProfileBackground(ctx context.Context, userID int64, profileBgImg string) (userdomain.User, error) {
	if userID <= 0 {
		// 背景图只能改当前登录用户，gateway 传入 token 中的 user_id，service 层继续做兜底校验。
		return userdomain.User{}, xerr.New(xerr.InvalidArgument, "user_id is required")
	}
	if s.userRepository == nil {
		return userdomain.User{}, xerr.New(xerr.Unavailable, "user repository is not configured")
	}
	if err := s.requireCompletedProfile(ctx, userID); err != nil {
		// 个人信息页背景图属于已完成资料后的用户资料维护，不能绕过注册资料完成流程。
		return userdomain.User{}, err
	}

	profileBgImg = strings.TrimSpace(profileBgImg)
	if profileBgImg == "" {
		// 上传接口必须写入真实对象 URL；清空背景需要另行设计明确的删除入口。
		return userdomain.User{}, xerr.New(xerr.InvalidArgument, "profile_bg_img is required")
	}
	if userdomain.ProfileStringTooLong(profileBgImg, userdomain.ProfileAvatarMaxRunes) {
		// users.profile_bg_img 与头像同为 512 字符 URL 字段，超过数据库边界必须在业务层拦截。
		return userdomain.User{}, xerr.New(xerr.InvalidArgument, "profile_bg_img is too long")
	}
	if !userdomain.ValidProfileAvatarURL(profileBgImg) {
		// 背景图是客户端直接渲染的 URL，不接受资源 code、相对路径或非 HTTP(S) scheme。
		return userdomain.User{}, xerr.New(xerr.InvalidArgument, "profile_bg_img must be an absolute http or https URL")
	}

	return s.userRepository.UpdateUserProfileBackground(ctx, userdomain.ProfileBackgroundUpdateCommand{
		AppCode:      appcode.FromContext(ctx),
		UserID:       userID,
		ProfileBgImg: profileBgImg,
		UpdatedAtMs:  s.now().UnixMilli(),
	})
}

// UpdateUserContactInfo 修改用户级联系方式；Host、BD、Agency owner 和币商中心共用这一个字段展示。
func (s *Service) UpdateUserContactInfo(ctx context.Context, userID int64, contactInfo string) (userdomain.User, error) {
	if userID <= 0 {
		// 联系方式只能改当前登录用户或后台已解析出的真实用户，非法 user_id 直接拒绝，避免落到空用户。
		return userdomain.User{}, xerr.New(xerr.InvalidArgument, "user_id is required")
	}
	if s.userRepository == nil {
		return userdomain.User{}, xerr.New(xerr.Unavailable, "user repository is not configured")
	}
	if err := s.requireCompletedProfile(ctx, userID); err != nil {
		// 未完成注册资料的账号不能先写经营联系方式，否则后台会看到一个仍不可进入业务的半成品用户。
		return userdomain.User{}, err
	}

	contactInfo = strings.TrimSpace(contactInfo)
	if userdomain.ProfileStringTooLong(contactInfo, userdomain.ProfileContactInfoMaxRunes) {
		// users.contact_info 是短展示字段；提前拦截超长输入，避免 MySQL 截断造成 H5 显示值和用户输入不一致。
		return userdomain.User{}, xerr.New(xerr.InvalidArgument, "contact_info is too long")
	}

	return s.userRepository.UpdateUserContactInfo(ctx, userdomain.ProfileContactUpdateCommand{
		AppCode:     appcode.FromContext(ctx),
		UserID:      userID,
		ContactInfo: contactInfo,
		UpdatedAtMs: s.now().UnixMilli(),
	})
}

// UpdateUserWithdrawAddress 修改用户默认提现 USDT-TRC20 地址；钱包提现单创建时再读取并快照该值。
func (s *Service) UpdateUserWithdrawAddress(ctx context.Context, userID int64, usdtTRC20Address string) (userdomain.User, error) {
	if userID <= 0 {
		// 提现地址是用户资产相关资料，只能写明确用户；非法 user_id 直接拒绝，避免落到空用户。
		return userdomain.User{}, xerr.New(xerr.InvalidArgument, "user_id is required")
	}
	if s.userRepository == nil {
		return userdomain.User{}, xerr.New(xerr.Unavailable, "user repository is not configured")
	}
	if err := s.requireCompletedProfile(ctx, userID); err != nil {
		// 未完成注册资料的账号不能提前写提现资料，避免半成品账号进入资产链路。
		return userdomain.User{}, err
	}

	usdtTRC20Address = strings.TrimSpace(usdtTRC20Address)
	if usdtTRC20Address == "" {
		return userdomain.User{}, xerr.New(xerr.InvalidArgument, "withdraw_usdt_trc20_address is required")
	}
	if userdomain.ProfileStringTooLong(usdtTRC20Address, userdomain.WithdrawUSDTTRC20AddressMaxRunes) {
		// users.withdraw_usdt_trc20_address 是短地址字段；先按数据库边界拦截，避免 MySQL 截断。
		return userdomain.User{}, xerr.New(xerr.InvalidArgument, "withdraw_usdt_trc20_address is too long")
	}
	if !userdomain.ValidUSDTTRC20Address(usdtTRC20Address) {
		// H5 只做展示和提交，服务端必须兜底拦截明显不是 TRC20 的地址，防止后续人工打款拿到脏数据。
		return userdomain.User{}, xerr.New(xerr.InvalidArgument, "withdraw_usdt_trc20_address is invalid")
	}

	return s.userRepository.UpdateUserWithdrawAddress(ctx, userdomain.WithdrawAddressUpdateCommand{
		AppCode:          appcode.FromContext(ctx),
		UserID:           userID,
		USDTTRC20Address: usdtTRC20Address,
		UpdatedAtMs:      s.now().UnixMilli(),
	})
}

// CompleteOnboarding 是注册页唯一提交入口。
// 它必须一次性校验并写入 username/avatar/gender/country/region_id/profile_completed，避免客户端拆接口拼出半完成状态。
func (s *Service) CompleteOnboarding(ctx context.Context, userID int64, username string, avatar string, gender string, country string, inviteCode string, requestID string) (userdomain.User, error) {
	username = strings.TrimSpace(username)
	avatar = strings.TrimSpace(avatar)
	gender = strings.TrimSpace(gender)
	country = userdomain.NormalizeCountryCode(country)
	inviteCode = invitedomain.NormalizeCode(inviteCode)
	if userID <= 0 {
		return userdomain.User{}, xerr.New(xerr.InvalidArgument, "user_id is required")
	}
	if username == "" {
		return userdomain.User{}, xerr.New(xerr.InvalidArgument, "username is required")
	}
	if userdomain.ProfileStringTooLong(username, userdomain.ProfileUsernameMaxRunes) {
		return userdomain.User{}, xerr.New(xerr.InvalidArgument, "username is too long")
	}
	if avatar == "" {
		return userdomain.User{}, xerr.New(xerr.InvalidArgument, "avatar is required")
	}
	if userdomain.ProfileStringTooLong(avatar, userdomain.ProfileAvatarMaxRunes) {
		return userdomain.User{}, xerr.New(xerr.InvalidArgument, "avatar is too long")
	}
	if !userdomain.ValidProfileAvatarURL(avatar) {
		return userdomain.User{}, xerr.New(xerr.InvalidArgument, "avatar must be an absolute http or https URL")
	}
	if gender == "" {
		return userdomain.User{}, xerr.New(xerr.InvalidArgument, "gender is required")
	}
	if userdomain.ProfileStringTooLong(gender, userdomain.ProfileGenderMaxRunes) {
		return userdomain.User{}, xerr.New(xerr.InvalidArgument, "gender is too long")
	}
	if country == "" {
		return userdomain.User{}, xerr.New(xerr.InvalidArgument, "country is required")
	}
	if !userdomain.ValidCountryCode(country) {
		return userdomain.User{}, xerr.New(xerr.InvalidArgument, "country must use 2 to 3 uppercase letters")
	}
	if inviteCode != "" && !invitedomain.ValidCode(inviteCode) {
		// 邀请码不存在、格式错误或不可用都统一落到 INVALID_INVITE_CODE，避免邀请码枚举。
		return userdomain.User{}, xerr.New(xerr.InvalidInviteCode, "invalid invite code")
	}
	if s.userRepository == nil {
		return userdomain.User{}, xerr.New(xerr.Unavailable, "user repository is not configured")
	}
	if s.countryRegionRepository == nil {
		return userdomain.User{}, xerr.New(xerr.Unavailable, "country repository is not configured")
	}

	resolvedCountry, ok, err := s.countryRegionRepository.ResolveEnabledCountryByCode(ctx, country)
	if err != nil {
		return userdomain.User{}, err
	}
	if !ok {
		return userdomain.User{}, xerr.New(xerr.InvalidArgument, "country is not supported")
	}
	var regionID int64
	if region, ok, err := s.countryRegionRepository.ResolveActiveRegionByCountry(ctx, resolvedCountry.CountryCode); err != nil {
		return userdomain.User{}, err
	} else if ok {
		// 国家未命中显式区域时，user-service 会返回 region_id=0 的 GLOBAL 兜底桶。
		regionID = region.RegionID
	}

	return s.userRepository.CompleteOnboarding(ctx, userdomain.CompleteOnboardingCommand{
		AppCode:       appcode.FromContext(ctx),
		UserID:        userID,
		Username:      username,
		Avatar:        avatar,
		Gender:        gender,
		Country:       resolvedCountry.CountryCode,
		RegionID:      regionID,
		InviteCode:    inviteCode,
		RequestID:     strings.TrimSpace(requestID),
		CompletedAtMs: s.now().UnixMilli(),
	})
}

// ChangeUserCountry 是 App 用户自助修改国家入口；普通用户不再受冷却期限制，业务身份用户必须走后台治理流程。
func (s *Service) ChangeUserCountry(ctx context.Context, userID int64, country string, requestID string) (userdomain.User, int64, error) {
	if userID <= 0 {
		return userdomain.User{}, 0, xerr.New(xerr.InvalidArgument, "user_id is required")
	}
	if s.userRepository == nil {
		return userdomain.User{}, 0, xerr.New(xerr.Unavailable, "user repository is not configured")
	}
	if err := s.requireCompletedProfile(ctx, userID); err != nil {
		// 未完成 onboarding 时拒绝在这里改国家，避免首次国家选择写入变更日志并消耗冷却期。
		return userdomain.User{}, 0, err
	}
	if s.roleSummaryRepository != nil {
		summary, err := s.roleSummaryRepository.GetUserRoleSummary(ctx, userID)
		if err != nil {
			return userdomain.User{}, 0, err
		}
		if role := blockedCountryChangeRole(summary); role != "" {
			// App 自助入口不能改变已有经营身份的区域归属，否则房间、组织和币商读模型会在用户侧无审计地漂移。
			// 使用独立公开 reason，避免 gateway 把普通 PERMISSION_DENIED 脱敏成语义不明的固定文案。
			return userdomain.User{}, 0, xerr.New(
				xerr.CountryChangeIdentityLocked,
				"You already have a host identity. Please contact customer service to change your country.",
			)
		}
	}
	resolvedCountry, regionID, err := s.resolveCountryRegionForChange(ctx, country)
	if err != nil {
		return userdomain.User{}, 0, err
	}

	nowMs := s.now().UnixMilli()
	command := userdomain.CountryChangeCommand{
		AppCode:     appcode.FromContext(ctx),
		UserID:      userID,
		NewCountry:  resolvedCountry.CountryCode,
		NewRegionID: regionID,
		Source:      "app",
		RequestID:   strings.TrimSpace(requestID),
		ChangedAtMs: nowMs,
		CooldownMs:  0,
	}
	updated, nextAllowedAt, revokedSessionIDs, err := s.userRepository.ChangeUserCountry(ctx, command)
	if err != nil {
		return userdomain.User{}, nextAllowedAt, err
	}
	s.writeCountryChangeSessionDenylist(ctx, command, revokedSessionIDs)
	return updated, nextAllowedAt, nil
}

// AdminChangeUserCountry 是后台修改用户国家入口；后台操作已在 admin-server 做 RBAC 和审计，这里只保证用户主数据和 outbox 原子一致。
func (s *Service) AdminChangeUserCountry(ctx context.Context, targetUserID int64, country string, requestID string) (userdomain.User, int64, error) {
	if targetUserID <= 0 {
		return userdomain.User{}, 0, xerr.New(xerr.InvalidArgument, "target_user_id is required")
	}
	if s.userRepository == nil {
		return userdomain.User{}, 0, xerr.New(xerr.Unavailable, "user repository is not configured")
	}
	resolvedCountry, regionID, err := s.resolveCountryRegionForChange(ctx, country)
	if err != nil {
		return userdomain.User{}, 0, err
	}

	nowMs := s.now().UnixMilli()
	command := userdomain.CountryChangeCommand{
		AppCode:     appcode.FromContext(ctx),
		UserID:      targetUserID,
		NewCountry:  resolvedCountry.CountryCode,
		NewRegionID: regionID,
		Source:      "admin",
		RequestID:   strings.TrimSpace(requestID),
		ChangedAtMs: nowMs,
		CooldownMs:  0,
	}
	updated, nextAllowedAt, revokedSessionIDs, err := s.userRepository.ChangeUserCountry(ctx, command)
	if err != nil {
		return userdomain.User{}, nextAllowedAt, err
	}
	s.writeCountryChangeSessionDenylist(ctx, command, revokedSessionIDs)
	return updated, nextAllowedAt, nil
}

func (s *Service) writeCountryChangeSessionDenylist(ctx context.Context, command userdomain.CountryChangeCommand, sessionIDs []string) {
	if len(sessionIDs) == 0 || s.sessionDenylist == nil {
		// MySQL auth_sessions 是登录态的持久事实；Redis denylist 只用于让已经签出的 access token 在过期前更快失效。
		return
	}
	_, _ = s.writeSessionDenylist(ctx, UserStatusCommand{
		AppCode:      command.AppCode,
		TargetUserID: command.UserID,
		RequestID:    command.RequestID,
		NowMs:        command.ChangedAtMs,
	}, sessionIDs, countryChangeRevokeReason)
}

func (s *Service) resolveCountryRegionForChange(ctx context.Context, country string) (userdomain.Country, int64, error) {
	country = userdomain.NormalizeCountryCode(country)
	if country == "" {
		return userdomain.Country{}, 0, xerr.New(xerr.InvalidArgument, "country is required")
	}
	if !userdomain.ValidCountryCode(country) {
		return userdomain.Country{}, 0, xerr.New(xerr.InvalidArgument, "country must use 2 to 3 uppercase letters")
	}
	if s.countryRegionRepository == nil {
		return userdomain.Country{}, 0, xerr.New(xerr.Unavailable, "country repository is not configured")
	}
	resolvedCountry, ok, err := s.countryRegionRepository.ResolveEnabledCountryByCode(ctx, country)
	if err != nil {
		return userdomain.Country{}, 0, err
	}
	if !ok {
		return userdomain.Country{}, 0, xerr.New(xerr.InvalidArgument, "country is not supported")
	}
	var regionID int64
	if region, ok, err := s.countryRegionRepository.ResolveActiveRegionByCountry(ctx, resolvedCountry.CountryCode); err != nil {
		return userdomain.Country{}, 0, err
	} else if ok {
		regionID = region.RegionID
	}

	return resolvedCountry, regionID, nil
}

func blockedCountryChangeRole(summary hostdomain.UserRoleSummary) string {
	switch {
	case summary.IsHost:
		return "host"
	case summary.IsAgency:
		return "agency"
	case summary.IsBDLeader:
		return "bd leader"
	case summary.IsBD:
		return "bd"
	case summary.IsManager:
		return "manager"
	case summary.IsCoinSeller:
		return "coin seller"
	default:
		return ""
	}
}

func (s *Service) requireCompletedProfile(ctx context.Context, userID int64) error {
	if s.userRepository == nil {
		return xerr.New(xerr.Unavailable, "user repository is not configured")
	}

	user, err := s.userRepository.GetUser(ctx, userID)
	if err != nil {
		// GetUser 已经把缺失用户映射成稳定 NOT_FOUND，这里不改写错误语义。
		return err
	}
	if !user.ProfileCompleted {
		// service 层必须基于数据库事实兜底，而不是只信 gateway token 里的 profile_completed 快照。
		return xerr.New(xerr.ProfileRequired, "profile required")
	}

	return nil
}

func trimmedStringPtr(value *string) *string {
	if value == nil {
		return nil
	}
	trimmed := strings.TrimSpace(*value)
	return &trimmed
}
