package userapi

import (
	"log/slog"
	"net/http"
	"strconv"
	"strings"

	activityv1 "hyapp.local/api/proto/activity/v1"
	userv1 "hyapp.local/api/proto/user/v1"
	"hyapp/pkg/logx"
	"hyapp/services/gateway-service/internal/auth"
	"hyapp/services/gateway-service/internal/transport/http/httpkit"
)

// userIdentityData 是 gateway 对外返回的当前短号投影。
// `display_user_id` 永远表示当前有效展示号，临时靓号过期后由 user-service 恢复为默认短号。
type userIdentityData struct {
	UserID                   string `json:"user_id"`
	DisplayUserID            string `json:"display_user_id"`
	PrettyID                 string `json:"pretty_id,omitempty"`
	PrettyDisplayUserID      string `json:"pretty_display_user_id,omitempty"`
	Status                   string `json:"status"`
	DefaultDisplayUserID     string `json:"default_display_user_id"`
	DisplayUserIDKind        string `json:"display_user_id_kind"`
	DisplayUserIDExpiresAtMs int64  `json:"display_user_id_expires_at_ms"`
	LeaseID                  string `json:"lease_id,omitempty"`
}

type resolveDisplayUserData struct {
	userIdentityData
	Identity userIdentityData `json:"identity"`
	Profile  userProfileData  `json:"profile,omitempty"`
}

// userProfileData 是 gateway 对外返回的用户基础资料投影。
// 它不包含注册来源、三方身份或 refresh session 等服务端内部字段。
type userProfileData struct {
	UserID                   string `json:"user_id"`
	DisplayUserID            string `json:"display_user_id"`
	PrettyID                 string `json:"pretty_id,omitempty"`
	PrettyDisplayUserID      string `json:"pretty_display_user_id,omitempty"`
	DefaultDisplayUserID     string `json:"default_display_user_id"`
	DisplayUserIDKind        string `json:"display_user_id_kind"`
	DisplayUserIDExpiresAtMs int64  `json:"display_user_id_expires_at_ms"`
	Username                 string `json:"username"`
	Gender                   string `json:"gender,omitempty"`
	Country                  string `json:"country,omitempty"`
	CountryID                int64  `json:"country_id,omitempty"`
	CountryName              string `json:"country_name,omitempty"`
	CountryDisplayName       string `json:"country_display_name,omitempty"`
	ISONumeric               string `json:"iso_numeric,omitempty"`
	PhoneCountryCode         string `json:"phone_country_code,omitempty"`
	CountryEnabled           bool   `json:"country_enabled"`
	RegionID                 int64  `json:"region_id,omitempty"`
	RegionCode               string `json:"region_code,omitempty"`
	RegionName               string `json:"region_name,omitempty"`
	Avatar                   string `json:"avatar,omitempty"`
	Birth                    string `json:"birth,omitempty"`
	Language                 string `json:"language,omitempty"`
	ProfileCompleted         bool   `json:"profile_completed"`
	ProfileCompletedAtMs     int64  `json:"profile_completed_at_ms"`
	OnboardingStatus         string `json:"onboarding_status"`
	CreatedAtMs              int64  `json:"created_at_ms"`
	UpdatedAtMs              int64  `json:"updated_at_ms"`
	ProfileBgImg             string `json:"profile_bg_img"`
	ContactInfo              string `json:"contact_info,omitempty"`
	NextCountryChangeAtMs    int64  `json:"next_country_change_allowed_at_ms,omitempty"`
}

type prettyDisplayIDData struct {
	PrettyID      string `json:"pretty_id"`
	DisplayUserID string `json:"display_user_id"`
	LevelTrack    string `json:"level_track"`
	MinLevel      int32  `json:"min_level"`
	MaxLevel      int32  `json:"max_level"`
}

// completeOnboardingData 在保留资料字段顶层兼容性的同时返回新的 access token。
// token.refresh_token 首版为空，客户端继续持有登录时的 refresh token，仅替换 access_token。
type completeOnboardingData struct {
	userProfileData
	Invite *inviteBindingData `json:"invite,omitempty"`
	Token  authTokenData      `json:"token"`
}

type inviteBindingData struct {
	Bound         bool   `json:"bound"`
	InviteCode    string `json:"invite_code,omitempty"`
	InviterUserID string `json:"inviter_user_id,omitempty"`
}

type inviteOverviewData struct {
	MyInviteCode             string `json:"my_invite_code"`
	InviteEnabled            bool   `json:"invite_enabled"`
	InviteCount              int64  `json:"invite_count"`
	ValidInviteCount         int64  `json:"valid_invite_count"`
	ValidInviteThresholdCoin int64  `json:"valid_invite_threshold_coin"`
}

// countryData 是注册页国家列表的公开响应投影。
// 只包含客户端选择和展示所需字段，不暴露管理端审计字段。
type countryData struct {
	CountryID          int64  `json:"country_id"`
	CountryName        string `json:"country_name"`
	CountryCode        string `json:"country_code"`
	ISOAlpha3          string `json:"iso_alpha3,omitempty"`
	ISONumeric         string `json:"iso_numeric,omitempty"`
	CountryDisplayName string `json:"country_display_name"`
	PhoneCountryCode   string `json:"phone_country_code,omitempty"`
	Enabled            bool   `json:"enabled"`
	Flag               string `json:"flag,omitempty"`
	SortOrder          int32  `json:"sort_order"`
}

// loginRiskBlockedCountryData 是 App 登录后异步地区风控只读词表。
// keyword 保留后台录入词，country_code 为空时表示该词只供客户端本地匹配。
type loginRiskBlockedCountryData struct {
	CountryCode string `json:"country_code,omitempty"`
	Keyword     string `json:"keyword"`
	UpdatedAtMs int64  `json:"updated_at_ms"`
}

// listRegistrationCountries 返回 App 注册页可选国家。
// 该接口公开读，不要求 access token；user-service 负责只返回 active 且 enabled 的国家。
func (h *Handler) listRegistrationCountries(writer http.ResponseWriter, request *http.Request) {
	if h.userCountryClient == nil {
		httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
		return
	}

	resp, err := h.userCountryClient.ListRegistrationCountries(request.Context(), &userv1.ListRegistrationCountriesRequest{
		Meta: httpkit.UserMeta(request, ""),
	})
	if err != nil {
		httpkit.WriteRPCError(writer, request, err)
		return
	}

	countries := make([]countryData, 0, len(resp.GetCountries()))
	for _, country := range resp.GetCountries() {
		countries = append(countries, registrationCountryData(country))
	}
	httpkit.WriteOK(writer, request, map[string]any{"countries": countries})
}

// listLoginRiskBlockedCountries 返回 App 可异步拉取的地区屏蔽词表。
// 登录最终处置仍由 user-service 风控 worker 决定，客户端只做补充风险探测和上报。
func (h *Handler) listLoginRiskBlockedCountries(writer http.ResponseWriter, request *http.Request) {
	if h.userCountryClient == nil {
		httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
		return
	}

	resp, err := h.userCountryClient.ListLoginRiskBlockedCountries(request.Context(), &userv1.ListLoginRiskBlockedCountriesRequest{
		Meta: httpkit.UserMeta(request, ""),
	})
	if err != nil {
		httpkit.WriteRPCError(writer, request, err)
		return
	}

	countries := make([]loginRiskBlockedCountryData, 0, len(resp.GetCountries()))
	for _, country := range resp.GetCountries() {
		countries = append(countries, loginRiskBlockedCountryData{
			CountryCode: country.GetCountryCode(),
			Keyword:     country.GetKeyword(),
			UpdatedAtMs: country.GetUpdatedAtMs(),
		})
	}
	httpkit.WriteOK(writer, request, map[string]any{"countries": countries, "updated_at_ms": resp.GetUpdatedAtMs()})
}

// getMyIdentity 返回当前登录用户的短号状态。
func (h *Handler) getMyIdentity(writer http.ResponseWriter, request *http.Request) {
	if h.userIdentityClient == nil {
		httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
		return
	}

	resp, err := h.userIdentityClient.GetUserIdentity(request.Context(), &userv1.GetUserIdentityRequest{
		Meta:   httpkit.UserMeta(request, ""),
		UserId: auth.UserIDFromContext(request.Context()),
	})
	if err != nil {
		httpkit.WriteRPCError(writer, request, err)
		return
	}

	identity := resp.GetIdentity()
	identityView := identityData(identity, "")
	data := resolveDisplayUserData{
		userIdentityData: identityView,
		Identity:         identityView,
	}
	if h.userProfileClient != nil && identity.GetUserId() > 0 {
		profileResp, err := h.userProfileClient.GetUser(request.Context(), &userv1.GetUserRequest{
			Meta:   httpkit.UserMeta(request, ""),
			UserId: identity.GetUserId(),
		})
		if err != nil {
			logx.Warn(request.Context(), "resolve_display_user_profile_failed", slog.Int64("user_id", identity.GetUserId()), slog.String("error", err.Error()))
		} else {
			data.Profile = profileData(profileResp.GetUser(), 0)
		}
	}
	httpkit.WriteOK(writer, request, data)
}

// resolveDisplayUserID 通过当前有效展示号解析用户。
func (h *Handler) resolveDisplayUserID(writer http.ResponseWriter, request *http.Request) {
	if h.userIdentityClient == nil {
		httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
		return
	}

	displayUserID := strings.TrimSpace(request.PathValue("display_user_id"))
	if displayUserID == "" {
		httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
		return
	}
	resp, err := h.userIdentityClient.ResolveDisplayUserID(request.Context(), &userv1.ResolveDisplayUserIDRequest{
		Meta:          httpkit.UserMeta(request, ""),
		DisplayUserId: displayUserID,
	})
	if err != nil {
		httpkit.WriteRPCError(writer, request, err)
		return
	}

	identity := resp.GetIdentity()
	identityView := identityData(identity, "")
	data := resolveDisplayUserData{
		userIdentityData: identityView,
		Identity:         identityView,
	}
	if h.userProfileClient != nil && identity.GetUserId() > 0 {
		// H5 搜索短号后要展示头像和昵称；短号解析只负责稳定 ID，资料展示必须回 user-service 用 user_id 再补齐。
		profileResp, err := h.userProfileClient.GetUser(request.Context(), &userv1.GetUserRequest{
			Meta:   httpkit.UserMeta(request, ""),
			UserId: identity.GetUserId(),
		})
		if err != nil {
			logx.Warn(request.Context(), "resolve_display_user_profile_failed", slog.Int64("user_id", identity.GetUserId()), slog.String("error", err.Error()))
		} else {
			data.Profile = profileData(profileResp.GetUser(), 0)
		}
	}
	httpkit.WriteOK(writer, request, data)
}

// completeMyOnboarding 是注册页唯一资料提交入口。
// gateway 只负责从 access token 取 user_id，资料校验和国家/区域解析必须由 user-service 同事务完成。
func (h *Handler) completeMyOnboarding(writer http.ResponseWriter, request *http.Request) {
	var body struct {
		Username   string `json:"username"`
		Avatar     string `json:"avatar"`
		Gender     string `json:"gender"`
		Country    string `json:"country"`
		InviteCode string `json:"invite_code"`
	}
	if !httpkit.Decode(writer, request, &body) {
		return
	}
	if h.userProfileClient == nil {
		httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
		return
	}

	resp, err := h.userProfileClient.CompleteOnboarding(request.Context(), &userv1.CompleteOnboardingRequest{
		Meta:       httpkit.UserMeta(request, ""),
		UserId:     auth.UserIDFromContext(request.Context()),
		Username:   body.Username,
		Avatar:     body.Avatar,
		Gender:     body.Gender,
		Country:    body.Country,
		InviteCode: body.InviteCode,
	})
	if err != nil {
		httpkit.WriteRPCError(writer, request, err)
		return
	}

	httpkit.WriteOK(writer, request, completeOnboardingData{
		userProfileData: profileData(resp.GetUser(), 0),
		Invite:          inviteBindingDataFromProto(resp.GetInvite()),
		Token:           authData(resp.GetToken(), nil),
	})
}

// getMyProfile 返回当前登录用户的基础资料。
func (h *Handler) getMyProfile(writer http.ResponseWriter, request *http.Request) {
	if h.userProfileClient == nil {
		httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
		return
	}

	resp, err := h.userProfileClient.GetUser(request.Context(), &userv1.GetUserRequest{
		Meta:   httpkit.UserMeta(request, ""),
		UserId: auth.UserIDFromContext(request.Context()),
	})
	if err != nil {
		httpkit.WriteRPCError(writer, request, err)
		return
	}

	httpkit.WriteOK(writer, request, profileData(resp.GetUser(), 0))
}

func (h *Handler) getUserProfile(writer http.ResponseWriter, request *http.Request) {
	if h.userProfileClient == nil {
		httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
		return
	}
	userID, ok := httpkit.PositiveInt64PathValue(request, "user_id")
	if !ok {
		httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
		return
	}

	// 成就页的 user_id 模式是只读展示目标用户资料，不能回退到 token 用户，否则分享页会出现资料和成就归属不一致。
	resp, err := h.userProfileClient.GetUser(request.Context(), &userv1.GetUserRequest{
		Meta:   httpkit.UserMeta(request, ""),
		UserId: userID,
	})
	if err != nil {
		httpkit.WriteRPCError(writer, request, err)
		return
	}

	httpkit.WriteOK(writer, request, profileData(resp.GetUser(), 0))
}

// getMyInviteOverview 返回当前登录用户自己的邀请码和邀请计数。
func (h *Handler) getMyInviteOverview(writer http.ResponseWriter, request *http.Request) {
	if h.userProfileClient == nil {
		httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
		return
	}

	resp, err := h.userProfileClient.GetUser(request.Context(), &userv1.GetUserRequest{
		Meta:   httpkit.UserMeta(request, ""),
		UserId: auth.UserIDFromContext(request.Context()),
	})
	if err != nil {
		httpkit.WriteRPCError(writer, request, err)
		return
	}

	httpkit.WriteOK(writer, request, inviteOverviewDataFromProto(resp.GetUser().GetInvite()))
}

// updateMyProfile 修改当前用户的用户名、头像、性别和生日；主页背景图走专用上传接口。
func (h *Handler) updateMyProfile(writer http.ResponseWriter, request *http.Request) {
	var body struct {
		Username       *string `json:"username"`
		Avatar         *string `json:"avatar"`
		AvatarUploadID *string `json:"avatar_upload_id"`
		Gender         *string `json:"gender"`
		Birth          *string `json:"birth"`
	}
	if !httpkit.Decode(writer, request, &body) {
		return
	}
	if h.userProfileClient == nil {
		httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
		return
	}

	resp, err := h.userProfileClient.UpdateUserProfile(request.Context(), &userv1.UpdateUserProfileRequest{
		Meta:           httpkit.UserMeta(request, ""),
		UserId:         auth.UserIDFromContext(request.Context()),
		Username:       body.Username,
		Avatar:         body.Avatar,
		AvatarUploadId: body.AvatarUploadID,
		Gender:         body.Gender,
		Birth:          body.Birth,
	})
	if err != nil {
		httpkit.WriteRPCError(writer, request, err)
		return
	}

	httpkit.WriteOK(writer, request, profileData(resp.GetUser(), 0))
}

// updateMyProfileBackground 修改当前用户个人信息页背景图；客户端先上传图片，本接口只保存 URL。
func (h *Handler) updateMyProfileBackground(writer http.ResponseWriter, request *http.Request) {
	var body struct {
		URL string `json:"url"`
	}
	if !httpkit.Decode(writer, request, &body) {
		return
	}
	if h.userProfileClient == nil {
		httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
		return
	}

	resp, err := h.userProfileClient.UpdateUserProfileBackground(request.Context(), &userv1.UpdateUserProfileBackgroundRequest{
		Meta:         httpkit.UserMeta(request, ""),
		UserId:       auth.UserIDFromContext(request.Context()),
		ProfileBgImg: body.URL,
	})
	if err != nil {
		httpkit.WriteRPCError(writer, request, err)
		return
	}

	httpkit.WriteOK(writer, request, map[string]any{
		"profile_bg_img": resp.GetUser().GetProfileBgImg(),
		"profile":        profileData(resp.GetUser(), 0),
	})
}

type contactInfoUpdateBody struct {
	ContactInfo        *string `json:"contact_info"`
	ContactInfoCamel   *string `json:"contactInfo"`
	WhatsappPhone      *string `json:"whatsapp_phone"`
	WhatsappPhoneCamel *string `json:"whatsappPhone"`
	Contact            *string `json:"contact"`
}

// updateMyContactInfo 修改当前登录用户的用户级联系方式；多个经营身份页面共用这一个字段。
func (h *Handler) updateMyContactInfo(writer http.ResponseWriter, request *http.Request) {
	var body contactInfoUpdateBody
	if !httpkit.Decode(writer, request, &body) {
		return
	}
	if h.userProfileClient == nil {
		httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
		return
	}
	contactInfo, ok := contactInfoFromBody(body)
	if !ok {
		httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "contact_info is required")
		return
	}

	resp, err := h.userProfileClient.UpdateUserContactInfo(request.Context(), &userv1.UpdateUserContactInfoRequest{
		Meta:        httpkit.UserMeta(request, ""),
		UserId:      auth.UserIDFromContext(request.Context()),
		ContactInfo: contactInfo,
	})
	if err != nil {
		httpkit.WriteRPCError(writer, request, err)
		return
	}

	// 兼容 H5 WhatsApp 字段命名，同时返回 profile，方便页面刷新本地用户资料。
	httpkit.WriteOK(writer, request, map[string]any{
		"contact_info":   resp.GetUser().GetContactInfo(),
		"whatsapp_phone": resp.GetUser().GetContactInfo(),
		"profile":        profileData(resp.GetUser(), 0),
	})
}

func contactInfoFromBody(body contactInfoUpdateBody) (string, bool) {
	// 按字段是否存在判断用户是否提交，允许空字符串清空联系方式。
	for _, value := range []*string{body.ContactInfo, body.ContactInfoCamel, body.WhatsappPhone, body.WhatsappPhoneCamel, body.Contact} {
		if value != nil {
			return strings.TrimSpace(*value), true
		}
	}
	return "", false
}

// changeMyCountry 单独修改当前用户国家；冷却期和审计由 user-service 保证。
func (h *Handler) changeMyCountry(writer http.ResponseWriter, request *http.Request) {
	var body struct {
		Country string `json:"country"`
	}
	if !httpkit.Decode(writer, request, &body) {
		return
	}
	if h.userProfileClient == nil {
		httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
		return
	}

	resp, err := h.userProfileClient.ChangeUserCountry(request.Context(), &userv1.ChangeUserCountryRequest{
		Meta:    httpkit.UserMeta(request, ""),
		UserId:  auth.UserIDFromContext(request.Context()),
		Country: body.Country,
	})
	if err != nil {
		httpkit.WriteRPCError(writer, request, err)
		return
	}
	h.removeOldRegionBroadcastMemberBestEffort(request, auth.UserIDFromContext(request.Context()), resp.GetOldRegionId(), resp.GetNewRegionId(), "user_country_changed")

	httpkit.WriteOK(writer, request, profileData(resp.GetUser(), resp.GetNextChangeAllowedAtMs()))
}

func (h *Handler) removeOldRegionBroadcastMemberBestEffort(request *http.Request, userID int64, oldRegionID int64, newRegionID int64, reason string) {
	if h == nil || h.broadcastClient == nil || userID <= 0 || oldRegionID <= 0 || oldRegionID == newRegionID {
		return
	}
	resp, err := h.broadcastClient.RemoveRegionBroadcastMember(request.Context(), &activityv1.RemoveRegionBroadcastMemberRequest{
		Meta:     httpkit.ActivityMeta(request),
		UserId:   userID,
		RegionId: oldRegionID,
		Reason:   reason,
	})
	if err != nil {
		// 国家修改已经由 user-service 提交，IM 旧群成员移除是外部副作用；失败记录日志，后续 callback 仍会阻止用户重新加入旧区域群。
		logx.Warn(request.Context(), "im_region_group_member_remove_failed",
			slog.Int64("user_id", userID),
			slog.Int64("old_region_id", oldRegionID),
			slog.Int64("new_region_id", newRegionID),
			slog.String("reason", reason),
			slog.String("error", err.Error()),
		)
		return
	}
	logx.Info(request.Context(), "im_region_group_member_removed",
		slog.Int64("user_id", userID),
		slog.Int64("old_region_id", oldRegionID),
		slog.Int64("new_region_id", newRegionID),
		slog.String("group_id", resp.GetGroupId()),
		slog.Bool("removed", resp.GetRemoved()),
	)
}

// changeMyDisplayUserID 修改当前登录用户的默认短号。
func (h *Handler) changeMyDisplayUserID(writer http.ResponseWriter, request *http.Request) {
	var body struct {
		NewDisplayUserID string `json:"new_display_user_id"`
		Reason           string `json:"reason"`
	}
	if !httpkit.Decode(writer, request, &body) {
		return
	}
	if h.userIdentityClient == nil {
		httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
		return
	}

	resp, err := h.userIdentityClient.ChangeDisplayUserID(request.Context(), &userv1.ChangeDisplayUserIDRequest{
		Meta:             httpkit.UserMeta(request, ""),
		UserId:           auth.UserIDFromContext(request.Context()),
		NewDisplayUserId: body.NewDisplayUserID,
		Reason:           body.Reason,
		OperatorUserId:   auth.UserIDFromContext(request.Context()),
	})
	if err != nil {
		httpkit.WriteRPCError(writer, request, err)
		return
	}

	httpkit.WriteOK(writer, request, identityData(resp.GetIdentity(), ""))
}

// applyMyPrettyDisplayUserID 为当前登录用户申请临时靓号。
func (h *Handler) applyMyPrettyDisplayUserID(writer http.ResponseWriter, request *http.Request) {
	var body struct {
		PrettyDisplayUserID string `json:"pretty_display_user_id"`
		LeaseDurationMs     int64  `json:"lease_duration_ms"`
		PaymentReceiptID    string `json:"payment_receipt_id"`
	}
	if !httpkit.Decode(writer, request, &body) {
		return
	}
	if h.userIdentityClient == nil {
		httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
		return
	}

	resp, err := h.userIdentityClient.ApplyPrettyDisplayUserID(request.Context(), &userv1.ApplyPrettyDisplayUserIDRequest{
		Meta:                httpkit.UserMeta(request, ""),
		UserId:              auth.UserIDFromContext(request.Context()),
		PrettyDisplayUserId: body.PrettyDisplayUserID,
		LeaseDurationMs:     body.LeaseDurationMs,
		PaymentReceiptId:    body.PaymentReceiptID,
	})
	if err != nil {
		httpkit.WriteRPCError(writer, request, err)
		return
	}

	httpkit.WriteOK(writer, request, identityData(resp.GetIdentity(), resp.GetLeaseId()))
}

// listAvailablePrettyDisplayIDs 返回当前登录用户等级可申请的靓号池号码。
func (h *Handler) listAvailablePrettyDisplayIDs(writer http.ResponseWriter, request *http.Request) {
	if h.userIdentityClient == nil {
		httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
		return
	}
	page, ok := httpkit.PositiveInt32Query(request, "page", 1)
	if !ok {
		httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
		return
	}
	pageSize, ok := httpkit.PositiveInt32Query(request, "page_size", 20)
	if !ok {
		httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
		return
	}
	// App 只能传分页参数；用户等级、池状态和号码可用性全部由 user-service 按当前登录用户重新计算。
	resp, err := h.userIdentityClient.ListAvailablePrettyDisplayIDs(request.Context(), &userv1.ListAvailablePrettyDisplayIDsRequest{
		Meta:     httpkit.UserMeta(request, ""),
		UserId:   auth.UserIDFromContext(request.Context()),
		Page:     page,
		PageSize: pageSize,
	})
	if err != nil {
		httpkit.WriteRPCError(writer, request, err)
		return
	}
	items := make([]prettyDisplayIDData, 0, len(resp.GetItems()))
	for _, item := range resp.GetItems() {
		// 这里返回申请所需 pretty_id 和展示用 display_user_id，等级区间只用于客户端展示申请门槛。
		items = append(items, prettyDisplayIDData{
			PrettyID:      item.GetPrettyId(),
			DisplayUserID: item.GetDisplayUserId(),
			LevelTrack:    item.GetPool().GetLevelTrack(),
			MinLevel:      item.GetPool().GetMinLevel(),
			MaxLevel:      item.GetPool().GetMaxLevel(),
		})
	}
	httpkit.WriteOK(writer, request, map[string]any{"items": items, "total": resp.GetTotal(), "page": page, "page_size": pageSize})
}

// applyPrettyDisplayIDFromPool 按 pretty_id 申请靓号池号码，租期由 user-service 固定为长期。
func (h *Handler) applyPrettyDisplayIDFromPool(writer http.ResponseWriter, request *http.Request) {
	var body struct {
		PrettyID string `json:"pretty_id"`
	}
	if !httpkit.Decode(writer, request, &body) {
		return
	}
	if h.userIdentityClient == nil {
		httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
		return
	}
	// 新靓号池申请只接收 pretty_id，不接受客户端传靓号内容或租期，防止绕过池状态和等级区间。
	resp, err := h.userIdentityClient.ApplyPrettyDisplayIDFromPool(request.Context(), &userv1.ApplyPrettyDisplayIDFromPoolRequest{
		Meta:     httpkit.UserMeta(request, ""),
		UserId:   auth.UserIDFromContext(request.Context()),
		PrettyId: strings.TrimSpace(body.PrettyID),
	})
	if err != nil {
		httpkit.WriteRPCError(writer, request, err)
		return
	}
	httpkit.WriteOK(writer, request, identityData(resp.GetIdentity(), resp.GetLeaseId()))
}

func identityData(identity *userv1.UserIdentity, leaseID string) userIdentityData {
	if identity == nil {
		return userIdentityData{LeaseID: leaseID}
	}

	// identity 投影必须带上 pretty_id 和 pretty_display_user_id，App 可区分真实用户 ID、默认短号和当前靓号。
	return userIdentityData{
		UserID:                   userIDStringFromIdentity(identity),
		DisplayUserID:            identity.GetDisplayUserId(),
		PrettyID:                 identity.GetPrettyId(),
		PrettyDisplayUserID:      identity.GetPrettyDisplayUserId(),
		Status:                   identity.GetStatus(),
		DefaultDisplayUserID:     identity.GetDefaultDisplayUserId(),
		DisplayUserIDKind:        identity.GetDisplayUserIdKind(),
		DisplayUserIDExpiresAtMs: identity.GetDisplayUserIdExpiresAtMs(),
		LeaseID:                  leaseID,
	}
}

func profileData(user *userv1.User, nextCountryChangeAtMs int64) userProfileData {
	if user == nil {
		return userProfileData{NextCountryChangeAtMs: nextCountryChangeAtMs}
	}

	// 所有资料卡共用该投影，pretty 字段在个人信息、房间资料卡等调用点保持同名返回。
	return userProfileData{
		UserID:                   userIDStringFromUser(user),
		DisplayUserID:            user.GetDisplayUserId(),
		PrettyID:                 user.GetPrettyId(),
		PrettyDisplayUserID:      user.GetPrettyDisplayUserId(),
		DefaultDisplayUserID:     user.GetDefaultDisplayUserId(),
		DisplayUserIDKind:        user.GetDisplayUserIdKind(),
		DisplayUserIDExpiresAtMs: user.GetDisplayUserIdExpiresAtMs(),
		Username:                 user.GetUsername(),
		Gender:                   user.GetGender(),
		Country:                  user.GetCountry(),
		CountryID:                user.GetCountryId(),
		CountryName:              user.GetCountryName(),
		CountryDisplayName:       user.GetCountryDisplayName(),
		ISONumeric:               user.GetIsoNumeric(),
		PhoneCountryCode:         user.GetPhoneCountryCode(),
		CountryEnabled:           user.GetCountryEnabled(),
		RegionID:                 user.GetRegionId(),
		RegionCode:               user.GetRegionCode(),
		RegionName:               user.GetRegionName(),
		Avatar:                   user.GetAvatar(),
		Birth:                    user.GetBirth(),
		Language:                 user.GetLanguage(),
		ProfileCompleted:         user.GetProfileCompleted(),
		ProfileCompletedAtMs:     user.GetProfileCompletedAtMs(),
		OnboardingStatus:         user.GetOnboardingStatus(),
		CreatedAtMs:              user.GetCreatedAtMs(),
		UpdatedAtMs:              user.GetUpdatedAtMs(),
		ProfileBgImg:             user.GetProfileBgImg(),
		ContactInfo:              user.GetContactInfo(),
		NextCountryChangeAtMs:    nextCountryChangeAtMs,
	}
}

func inviteBindingDataFromProto(binding *userv1.InviteBinding) *inviteBindingData {
	if binding == nil {
		return nil
	}
	return &inviteBindingData{
		Bound:         binding.GetBound(),
		InviteCode:    binding.GetInviteCode(),
		InviterUserID: userIDString(binding.GetInviterUserId()),
	}
}

func inviteOverviewDataFromProto(overview *userv1.InviteOverview) inviteOverviewData {
	if overview == nil {
		return inviteOverviewData{}
	}
	return inviteOverviewData{
		MyInviteCode:             overview.GetMyInviteCode(),
		InviteEnabled:            overview.GetInviteEnabled(),
		InviteCount:              overview.GetInviteCount(),
		ValidInviteCount:         overview.GetValidInviteCount(),
		ValidInviteThresholdCoin: overview.GetValidInviteThresholdCoin(),
	}
}

func userIDString(userID int64) string {
	if userID > 0 {
		return strconv.FormatInt(userID, 10)
	}
	return ""
}

func userIDStringFromUser(user *userv1.User) string {
	if user == nil {
		return ""
	}
	return userIDString(user.GetUserId())
}

func userIDStringFromIdentity(identity *userv1.UserIdentity) string {
	if identity == nil {
		return ""
	}
	return userIDString(identity.GetUserId())
}

func registrationCountryData(country *userv1.Country) countryData {
	if country == nil {
		return countryData{}
	}

	return countryData{
		CountryID:          country.GetCountryId(),
		CountryName:        country.GetCountryName(),
		CountryCode:        country.GetCountryCode(),
		ISOAlpha3:          country.GetIsoAlpha3(),
		ISONumeric:         country.GetIsoNumeric(),
		CountryDisplayName: country.GetCountryDisplayName(),
		PhoneCountryCode:   country.GetPhoneCountryCode(),
		Enabled:            country.GetEnabled(),
		Flag:               country.GetFlag(),
		SortOrder:          country.GetSortOrder(),
	}
}
