package managerapi

import (
	"encoding/json"
	"hyapp/services/gateway-service/internal/transport/http/httpkit"
	"log/slog"
	"net/http"
	"strconv"
	"strings"

	activityv1 "hyapp.local/api/proto/activity/v1"
	userv1 "hyapp.local/api/proto/user/v1"
	walletv1 "hyapp.local/api/proto/wallet/v1"
	"hyapp/pkg/appcode"
	"hyapp/pkg/logx"
	"hyapp/services/gateway-service/internal/auth"
)

const managerCenterCapability = "manager_center"
const managerResourceGrantCapability = "manager_resource_grant"
const managerGrantAvatarFrameCapability = "manager_grant_avatar_frame"
const managerGrantVehicleCapability = "manager_grant_vehicle"
const managerGrantBadgeCapability = "manager_grant_badge"
const managerGrantVIPCapability = "manager_grant_vip"
const managerUpdateUserLevelCapability = "manager_update_user_level"
const managerAddBDLeaderCapability = "manager_add_bd_leader"
const managerCrossRegionCapability = "manager_cross_region"
const managerAddAdminCapability = "manager_add_admin"
const managerAddSuperadminCapability = "manager_add_superadmin"
const managerBlockUserCapability = "manager_block_user"
const managerTransferUserCountryCapability = "manager_transfer_user_country"
const managerGrantDayMS int64 = 24 * 60 * 60 * 1000
const managerGrantDefaultDurationMS int64 = 7 * managerGrantDayMS
const managerGrantVIPDurationMS int64 = 30 * managerGrantDayMS
const managerGrantMaxResourceIDs = 20
const managerGrantMaxVIPLevel int32 = 5

const managerRoleScopeCountry = "country"
const managerRoleScopeRegion = "region"
const managerRoleScopeGlobal = "global"
const managerOperationScopeScene = "manager_operations"

type managerGrantResourceData struct {
	ResourceID        string `json:"resource_id"`
	ResourceType      string `json:"resource_type"`
	Name              string `json:"name"`
	AssetURL          string `json:"asset_url"`
	PreviewURL        string `json:"preview_url"`
	WalletAssetType   string `json:"wallet_asset_type"`
	WalletAssetAmount int64  `json:"wallet_asset_amount"`
	SortOrder         int32  `json:"sort_order"`
}

type businessUserLookupData struct {
	UserID        string `json:"user_id"`
	DisplayUserID string `json:"display_user_id"`
	Username      string `json:"username"`
	Avatar        string `json:"avatar"`
	Status        string `json:"status"`
	RegionID      int64  `json:"region_id,omitempty"`
	RegionCode    string `json:"region_code,omitempty"`
	RegionName    string `json:"region_name,omitempty"`
}

type managerUserData struct {
	UserID        string                      `json:"user_id"`
	DisplayUserID string                      `json:"display_user_id"`
	Username      string                      `json:"username"`
	Avatar        string                      `json:"avatar"`
	Status        string                      `json:"status"`
	Country       string                      `json:"country"`
	RegionID      int64                       `json:"region_id,omitempty"`
	RegionCode    string                      `json:"region_code,omitempty"`
	RegionName    string                      `json:"region_name,omitempty"`
	Levels        map[string]managerLevelData `json:"levels"`
	CurrentVIP    *managerVipData             `json:"current_vip,omitempty"`
}

type managerLevelData struct {
	Track  string `json:"track"`
	Level  int32  `json:"level"`
	TierID string `json:"tier_id,omitempty"`
}

type managerVipData struct {
	Level       int32  `json:"level"`
	Name        string `json:"name"`
	Active      bool   `json:"active"`
	StartedAtMS int64  `json:"started_at_ms"`
	ExpiresAtMS int64  `json:"expires_at_ms"`
	UpdatedAtMS int64  `json:"updated_at_ms"`
}

type managerResourceGrantData struct {
	GrantID        string `json:"grant_id"`
	CommandID      string `json:"command_id"`
	TargetUserID   string `json:"target_user_id"`
	ResourceID     string `json:"resource_id"`
	Status         string `json:"status"`
	GrantSource    string `json:"grant_source"`
	OperatorUserID string `json:"operator_user_id"`
	CreatedAtMS    int64  `json:"created_at_ms"`
}

type managerBlockData struct {
	BlockID             string `json:"block_id"`
	TargetUserID        string `json:"target_user_id"`
	TargetDisplayUserID string `json:"target_display_user_id"`
	TargetUsername      string `json:"target_username"`
	TargetAvatar        string `json:"target_avatar"`
	Country             string `json:"country"`
	BlockedUntilMS      int64  `json:"blocked_until_ms"`
	Status              string `json:"status"`
	Reason              string `json:"reason"`
	CreatedAtMS         int64  `json:"created_at_ms"`
	UpdatedAtMS         int64  `json:"updated_at_ms"`
}

type managerLevelUpdateData struct {
	EventID        string `json:"event_id"`
	Status         string `json:"status"`
	Track          string `json:"track"`
	PreviousLevel  int32  `json:"previous_level"`
	NewLevel       int32  `json:"new_level"`
	TotalValue     int64  `json:"total_value"`
	RewardJobCount int64  `json:"reward_job_count"`
	ServerTimeMS   int64  `json:"server_time_ms"`
}

type managerBDLeaderData struct {
	UserID             string `json:"user_id"`
	Role               string `json:"role"`
	RegionID           int64  `json:"region_id"`
	ParentLeaderUserID string `json:"parent_leader_user_id"`
	Status             string `json:"status"`
	CreatedByUserID    string `json:"created_by_user_id"`
	CreatedAtMS        int64  `json:"created_at_ms"`
	UpdatedAtMS        int64  `json:"updated_at_ms"`
}

func (h *Handler) getManagerOverview(writer http.ResponseWriter, request *http.Request) {
	if h.userHostClient == nil || h.userProfileClient == nil {
		httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
		return
	}
	// 经理中心首页只信 user-service 的角色事实；前端传 is_manager 或 identity 都不能绕过这里。
	actor, ok := h.requireManagerCenterActor(writer, request)
	if !ok {
		return
	}
	payload := map[string]any{
		"is_manager":      true,
		"user_id":         httpkit.UserIDString(actor.GetUserId()),
		"display_user_id": actor.GetDisplayUserId(),
		"username":        actor.GetUsername(),
		"avatar":          actor.GetAvatar(),
		"country":         actor.GetCountry(),
		"region_id":       actor.GetRegionId(),
		"region_code":     actor.GetRegionCode(),
		"region_name":     actor.GetRegionName(),
		"permissions":     h.managerPermissionOverview(request),
	}
	// 团队工资统计是增强信息：聚合失败只丢卡片数据不阻断经理中心，H5 对缺失字段做占位展示。
	if dashboard := h.managerTeamDashboard(request, actor.GetUserId()); dashboard != nil {
		payload["dashboard"] = dashboard
	}
	httpkit.WriteOK(writer, request, payload)
}

func (h *Handler) searchManagerUsers(writer http.ResponseWriter, request *http.Request) {
	if h.userProfileClient == nil || h.userHostClient == nil {
		httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
		return
	}
	// 搜索先走已有 business lookup 保持短号/昵称解析一致，再用 BatchGetUsers 拿国家和区域投影做最终过滤。
	// 所有经理动作的 App 差异都来自 user-service 策略，handler 不直接判断 app_code。
	actor, ok := h.requireManagerCenterActor(writer, request)
	if !ok {
		return
	}
	managerTargetScope, ok := h.managerEffectiveScope(writer, request)
	if !ok {
		return
	}
	pageSize, ok := httpkit.PositiveInt32Query(request, "page_size", 20)
	if !ok {
		httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
		return
	}
	if pageSize > 20 {
		pageSize = 20
	}
	lookupResp, err := h.userProfileClient.BusinessUserLookup(request.Context(), &userv1.BusinessUserLookupRequest{
		Meta:        httpkit.UserMeta(request, ""),
		ActorUserId: actor.GetUserId(),
		Scene:       managerSearchScene(request),
		Keyword:     strings.TrimSpace(request.URL.Query().Get("keyword")),
		PageSize:    pageSize,
	})
	if err != nil {
		httpkit.WriteRPCError(writer, request, err)
		return
	}
	userIDs := make([]int64, 0, len(lookupResp.GetUsers()))
	seen := map[int64]bool{}
	for _, item := range lookupResp.GetUsers() {
		if id := item.GetUserId(); id > 0 && !seen[id] {
			seen[id] = true
			userIDs = append(userIDs, id)
		}
	}
	if len(userIDs) == 0 {
		httpkit.WriteOK(writer, request, map[string]any{"items": []managerUserData{}, "total": 0, "page_size": pageSize})
		return
	}
	usersResp, err := h.userProfileClient.BatchGetUsers(request.Context(), &userv1.BatchGetUsersRequest{
		Meta:    httpkit.UserMeta(request, ""),
		UserIds: userIDs,
	})
	if err != nil {
		httpkit.WriteRPCError(writer, request, err)
		return
	}
	levels := h.managerLevelsByUser(request, userIDs)
	items := make([]managerUserData, 0, len(userIDs))
	visibleUserIDs := make([]int64, 0, len(userIDs))
	for _, userID := range userIDs {
		user := usersResp.GetUsers()[userID]
		// 搜索与最终创建共用 scope evaluator，避免同一 App 的规则在两个 handler 分叉。
		if user == nil || user.GetStatus() != userv1.UserStatus_USER_STATUS_ACTIVE || !managerTargetMatchesScope(managerTargetScope, actor, user) {
			continue
		}
		item := managerUserFromProto(user, levels[userID])
		items = append(items, item)
		visibleUserIDs = append(visibleUserIDs, userID)
	}
	vips := h.managerCurrentVIPByUser(request, visibleUserIDs, managerSearchScene(request))
	for index, userID := range visibleUserIDs {
		items[index].CurrentVIP = vips[userID]
	}
	httpkit.WriteOK(writer, request, map[string]any{"items": items, "total": len(items), "page_size": pageSize})
}

func (h *Handler) listManagerGrantResources(writer http.ResponseWriter, request *http.Request) {
	if h.walletClient == nil || h.userHostClient == nil || h.userProfileClient == nil {
		httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
		return
	}
	if _, ok := h.requireManagerCenterActor(writer, request); !ok {
		return
	}
	resourceType := strings.TrimSpace(request.URL.Query().Get("resource_type"))
	if !h.requireManagerCapability(writer, request, managerCapabilityForResourceType(resourceType)) {
		return
	}
	page, pageSize, ok := managerPage(request)
	if !ok {
		httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
		return
	}
	resp, err := h.walletClient.ListResources(request.Context(), &walletv1.ListResourcesRequest{
		RequestId:        httpkit.RequestIDFromContext(request.Context()),
		AppCode:          appcode.FromContext(request.Context()),
		ResourceType:     resourceType,
		Page:             page,
		PageSize:         pageSize,
		ActiveOnly:       true,
		ManagerGrantOnly: true,
	})
	if err != nil {
		httpkit.WriteRPCError(writer, request, err)
		return
	}
	items := make([]managerGrantResourceData, 0, len(resp.GetResources()))
	for _, resource := range resp.GetResources() {
		items = append(items, managerGrantResourceFromProto(resource))
	}
	httpkit.WriteOK(writer, request, map[string]any{"items": items, "total": resp.GetTotal(), "page": page, "page_size": pageSize})
}

func (h *Handler) lookupBusinessUsers(writer http.ResponseWriter, request *http.Request) {
	if h.userProfileClient == nil {
		httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
		return
	}
	pageSize, ok := httpkit.PositiveInt32Query(request, "page_size", 20)
	if !ok {
		httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
		return
	}
	if pageSize > 20 {
		pageSize = 20
	}
	resp, err := h.userProfileClient.BusinessUserLookup(request.Context(), &userv1.BusinessUserLookupRequest{
		Meta:        httpkit.UserMeta(request, ""),
		ActorUserId: auth.UserIDFromContext(request.Context()),
		Scene:       strings.TrimSpace(request.URL.Query().Get("scene")),
		Keyword:     strings.TrimSpace(request.URL.Query().Get("keyword")),
		PageSize:    pageSize,
	})
	if err != nil {
		httpkit.WriteRPCError(writer, request, err)
		return
	}
	items := make([]businessUserLookupData, 0, len(resp.GetUsers()))
	for _, user := range resp.GetUsers() {
		items = append(items, businessUserLookupFromProto(user))
	}
	httpkit.WriteOK(writer, request, map[string]any{"items": items, "total": len(items), "page_size": pageSize})
}

func (h *Handler) grantManagerResource(writer http.ResponseWriter, request *http.Request) {
	if h.walletClient == nil || h.userHostClient == nil || h.userProfileClient == nil {
		httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
		return
	}
	var body struct {
		CommandID       string            `json:"command_id"`
		CommandIDAlt    string            `json:"commandId"`
		TargetUserID    json.RawMessage   `json:"target_user_id"`
		TargetUserIDAlt json.RawMessage   `json:"targetUserId"`
		ResourceID      json.RawMessage   `json:"resource_id"`
		ResourceIDAlt   json.RawMessage   `json:"resourceId"`
		ResourceIDs     []json.RawMessage `json:"resource_ids"`
		ResourceIDsAlt  []json.RawMessage `json:"resourceIds"`
		Reason          string            `json:"reason"`
		Quantity        *int64            `json:"quantity"`
		DurationMS      *int64            `json:"duration_ms"`
		DurationMSAlt   *int64            `json:"durationMs"`
	}
	if !httpkit.Decode(writer, request, &body) {
		return
	}
	if body.Quantity != nil {
		httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
		return
	}
	durationMS, durationOK := managerGrantDurationMS(body.DurationMS, body.DurationMSAlt)
	if !durationOK {
		httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
		return
	}
	commandID := strings.TrimSpace(body.CommandID)
	if commandID == "" {
		commandID = strings.TrimSpace(body.CommandIDAlt)
	}
	targetUserID, targetOK := rawInt64(body.TargetUserID, body.TargetUserIDAlt)
	resourceIDs, listPresent, listOK := rawInt64List(body.ResourceIDs, body.ResourceIDsAlt)
	if !listPresent {
		resourceID, resourceOK := rawInt64(body.ResourceID, body.ResourceIDAlt)
		if resourceOK {
			resourceIDs = []int64{resourceID}
		}
	}
	if commandID == "" || !targetOK || targetUserID <= 0 || (listPresent && !listOK) || len(resourceIDs) == 0 || len(resourceIDs) > managerGrantMaxResourceIDs {
		httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
		return
	}
	resources := make([]*walletv1.Resource, 0, len(resourceIDs))
	for _, resourceID := range resourceIDs {
		if resourceID <= 0 {
			httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
			return
		}
		resourceResp, err := h.walletClient.GetResource(request.Context(), &walletv1.GetResourceRequest{
			RequestId:  httpkit.RequestIDFromContext(request.Context()),
			AppCode:    appcode.FromContext(request.Context()),
			ResourceId: resourceID,
		})
		if err != nil {
			httpkit.WriteRPCError(writer, request, err)
			return
		}
		resource := resourceResp.GetResource()
		if resource == nil || resource.GetResourceId() <= 0 {
			httpkit.WriteError(writer, request, http.StatusNotFound, httpkit.CodeNotFound, "not found")
			return
		}
		// 写入口不能信任 H5 自报的资源类型；必须逐个从 wallet-service 读取 resource_id 的真实类型，再按头像框、座驾或徽章权限拒绝。
		if !h.requireManagerCapability(writer, request, managerCapabilityForResourceType(resource.GetResourceType())) {
			return
		}
		resources = append(resources, resource)
	}
	// 所有经理目标用户动作共用同一有效范围；wallet-service 还会在事务内复验资源白名单。
	if _, ok := h.requireManagerTarget(writer, request, targetUserID); !ok {
		return
	}
	reason := strings.TrimSpace(body.Reason)
	if reason == "" {
		reason = "manager_center"
	}
	actorUserID := auth.UserIDFromContext(request.Context())
	grants := make([]managerResourceGrantData, 0, len(resources))
	for _, resource := range resources {
		resourceID := resource.GetResourceId()
		grantCommandID := commandID
		if len(resources) > 1 {
			// 批量素材赠送复用 wallet 的单资源事务；每个素材派生稳定 command_id，前半批成功后重试也只补未完成的资源。
			grantCommandID = commandID + ":resource:" + strconv.FormatInt(resourceID, 10)
		}
		resp, err := h.walletClient.GrantResource(request.Context(), &walletv1.GrantResourceRequest{
			CommandId:    grantCommandID,
			AppCode:      appcode.FromContext(request.Context()),
			TargetUserId: targetUserID,
			ResourceId:   resourceID,
			Quantity:     1,
			// 经理中心只能赠送产品允许的三个限时档位；数量、来源和操作人仍由网关固定，避免 H5 自行组装账务形状。
			DurationMs:     durationMS,
			Reason:         reason,
			OperatorUserId: actorUserID,
			GrantSource:    "manager_center",
		})
		if err != nil {
			logx.Warn(request.Context(), "manager_resource_grant_failed",
				slog.Int64("actor_user_id", actorUserID),
				slog.Int64("target_user_id", targetUserID),
				slog.Int64("resource_id", resourceID),
				slog.String("command_id", grantCommandID),
			)
			httpkit.WriteRPCError(writer, request, err)
			return
		}
		grant := managerResourceGrantFromProto(resp.GetGrant())
		logx.Info(request.Context(), "manager_resource_grant_succeeded",
			slog.Int64("actor_user_id", actorUserID),
			slog.Int64("target_user_id", targetUserID),
			slog.Int64("resource_id", resourceID),
			slog.String("grant_id", grant.GrantID),
		)
		grants = append(grants, grant)
	}
	if len(grants) == 1 {
		httpkit.WriteOK(writer, request, grants[0])
		return
	}
	httpkit.WriteOK(writer, request, map[string]any{"items": grants, "total": len(grants)})
}

func (h *Handler) grantManagerVIP(writer http.ResponseWriter, request *http.Request) {
	if h.walletClient == nil || h.userHostClient == nil || h.userProfileClient == nil {
		httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
		return
	}
	var body struct {
		CommandID       string          `json:"command_id"`
		CommandIDAlt    string          `json:"commandId"`
		TargetUserID    json.RawMessage `json:"target_user_id"`
		TargetUserIDAlt json.RawMessage `json:"targetUserId"`
		Level           int32           `json:"level"`
		Reason          string          `json:"reason"`
	}
	if !httpkit.Decode(writer, request, &body) {
		return
	}
	commandID := strings.TrimSpace(body.CommandID)
	if commandID == "" {
		commandID = strings.TrimSpace(body.CommandIDAlt)
	}
	targetUserID, targetOK := rawInt64(body.TargetUserID, body.TargetUserIDAlt)
	// 经理中心只放开 VIP1-5 的运营赠送入口；钱包仍按 vip_levels.status 做最终配置校验，避免 H5 放开后绕过等级启停。
	if commandID == "" || !targetOK || targetUserID <= 0 || body.Level < 1 || body.Level > managerGrantMaxVIPLevel {
		httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
		return
	}
	if !h.requireManagerCapability(writer, request, managerGrantVIPCapability) {
		return
	}
	// 经理中心先读取 App 级 program，再选择直接会员或体验卡专用命令。钱包会在写事务内再次校验，
	// 因此配置在两次 RPC 之间切换时只会明确失败，不会绕过当前发放语义。
	if _, ok := h.requireManagerTarget(writer, request, targetUserID); !ok {
		return
	}
	reason := strings.TrimSpace(body.Reason)
	if reason == "" {
		reason = "manager_center"
	}
	appCode := appcode.FromContext(request.Context())
	programResp, err := h.walletClient.GetVipProgramConfig(request.Context(), &walletv1.GetVipProgramConfigRequest{
		RequestId: httpkit.RequestIDFromContext(request.Context()),
		AppCode:   appCode,
	})
	if err != nil {
		httpkit.WriteRPCError(writer, request, err)
		return
	}
	program := programResp.GetConfig()
	if program == nil {
		httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
		return
	}
	if program.GetGrantMode() == "trial_card" {
		if !program.GetTrialCardEnabled() {
			httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
			return
		}
		resp, err := h.walletClient.GrantVipTrialCard(request.Context(), &walletv1.GrantVipTrialCardRequest{
			CommandId:      commandID,
			AppCode:        appCode,
			TargetUserId:   targetUserID,
			Level:          body.Level,
			DurationMs:     managerGrantVIPDurationMS,
			GrantSource:    "manager_center",
			OperatorUserId: auth.UserIDFromContext(request.Context()),
			Reason:         reason,
		})
		if err != nil {
			httpkit.WriteRPCError(writer, request, err)
			return
		}
		card := resp.GetTrialCard()
		httpkit.WriteOK(writer, request, map[string]any{
			"grant_mode":      "trial_card",
			"transaction_id":  "",
			"trial_card_id":   card.GetTrialCardId(),
			"entitlement_id":  card.GetEntitlementId(),
			"source_grant_id": card.GetSourceGrantId(),
			"level":           card.GetLevel(),
			// 发卡只入背包且从发卡时开始绝对计时；active=false 明确表示未直接激活会员。
			"active":         false,
			"card_status":    card.GetStatus(),
			"equipped":       card.GetEquipped(),
			"expires_at_ms":  card.GetExpiresAtMs(),
			"server_time_ms": resp.GetServerTimeMs(),
		})
		return
	}
	if program.GetGrantMode() != "direct_membership" {
		httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
		return
	}
	resp, err := h.walletClient.GrantVip(request.Context(), &walletv1.GrantVipRequest{
		CommandId:      commandID,
		AppCode:        appCode,
		TargetUserId:   targetUserID,
		Level:          body.Level,
		GrantSource:    "manager_center",
		OperatorUserId: auth.UserIDFromContext(request.Context()),
		Reason:         reason,
	})
	if err != nil {
		httpkit.WriteRPCError(writer, request, err)
		return
	}
	httpkit.WriteOK(writer, request, map[string]any{
		"grant_mode":     "direct_membership",
		"transaction_id": resp.GetTransactionId(),
		"level":          resp.GetVip().GetLevel(),
		"active":         resp.GetVip().GetActive(),
		"expires_at_ms":  resp.GetVip().GetExpiresAtMs(),
		"server_time_ms": resp.GetServerTimeMs(),
	})
}

func (h *Handler) removeManagerOldRegionBroadcastMemberBestEffort(request *http.Request, userID int64, oldRegionID int64, newRegionID int64) {
	if h == nil || h.broadcastClient == nil || userID <= 0 || oldRegionID <= 0 || oldRegionID == newRegionID {
		return
	}
	_, err := h.broadcastClient.RemoveRegionBroadcastMember(request.Context(), &activityv1.RemoveRegionBroadcastMemberRequest{
		Meta:     httpkit.ActivityMeta(request),
		UserId:   userID,
		RegionId: oldRegionID,
		Reason:   "manager_user_country_changed",
	})
	if err != nil {
		// 国家和房间迁移事实已经提交，腾讯 IM 旧群清理失败不能把已成功的国家修改伪装成失败。
		logx.Warn(request.Context(), "manager_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("error", err.Error()),
		)
	}
}

func (h *Handler) managerBlocks(writer http.ResponseWriter, request *http.Request) {
	switch request.Method {
	case http.MethodGet:
		h.listManagerBlocks(writer, request)
	case http.MethodPost:
		h.createManagerBlock(writer, request)
	default:
		httpkit.WriteError(writer, request, http.StatusMethodNotAllowed, httpkit.CodeInvalidArgument, "method not allowed")
	}
}

func (h *Handler) listManagerBlocks(writer http.ResponseWriter, request *http.Request) {
	if h.userProfileClient == nil || h.userHostClient == nil {
		httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
		return
	}
	actor, ok := h.requireManagerCenterActor(writer, request)
	if !ok {
		return
	}
	if !h.requireManagerCapability(writer, request, managerBlockUserCapability) {
		return
	}
	pageSize, ok := httpkit.PositiveInt32Query(request, "page_size", 50)
	if !ok {
		httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
		return
	}
	if pageSize > 100 {
		pageSize = 100
	}
	status := strings.TrimSpace(request.URL.Query().Get("status"))
	resp, err := h.userProfileClient.ListManagerUserBlocks(request.Context(), &userv1.ListManagerUserBlocksRequest{
		Meta:          httpkit.UserMeta(request, ""),
		ManagerUserId: actor.GetUserId(),
		Status:        status,
		PageSize:      pageSize,
	})
	if err != nil {
		httpkit.WriteRPCError(writer, request, err)
		return
	}
	items := make([]managerBlockData, 0, len(resp.GetBlocks()))
	for _, block := range resp.GetBlocks() {
		items = append(items, managerBlockFromProto(block))
	}
	httpkit.WriteOK(writer, request, map[string]any{"items": items, "total": len(items), "page_size": pageSize})
}

func (h *Handler) createManagerBlock(writer http.ResponseWriter, request *http.Request) {
	if h.userProfileClient == nil || h.userHostClient == nil {
		httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
		return
	}
	var body struct {
		CommandID         string          `json:"command_id"`
		CommandIDAlt      string          `json:"commandId"`
		TargetUserID      json.RawMessage `json:"target_user_id"`
		TargetUserIDAlt   json.RawMessage `json:"targetUserId"`
		BlockedUntilMS    int64           `json:"blocked_until_ms"`
		BlockedUntilMSAlt int64           `json:"blockedUntilMs"`
		Reason            string          `json:"reason"`
	}
	if !httpkit.Decode(writer, request, &body) {
		return
	}
	actor, ok := h.requireManagerCenterActor(writer, request)
	if !ok {
		return
	}
	if !h.requireManagerCapability(writer, request, managerBlockUserCapability) {
		return
	}
	commandID := strings.TrimSpace(body.CommandID)
	if commandID == "" {
		commandID = strings.TrimSpace(body.CommandIDAlt)
	}
	targetUserID, targetOK := rawInt64(body.TargetUserID, body.TargetUserIDAlt)
	blockedUntilMS := body.BlockedUntilMS
	if blockedUntilMS <= 0 {
		blockedUntilMS = body.BlockedUntilMSAlt
	}
	if commandID == "" || !targetOK || targetUserID <= 0 || blockedUntilMS <= 0 {
		httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
		return
	}
	// user-service 会按同一 App 策略复验，gateway 先拒绝越界目标以减少无效封禁事务。
	if _, ok := h.requireManagerTargetWithActor(writer, request, actor, targetUserID); !ok {
		return
	}
	resp, err := h.userProfileClient.CreateManagerUserBlock(request.Context(), &userv1.CreateManagerUserBlockRequest{
		Meta:           httpkit.UserMeta(request, ""),
		CommandId:      commandID,
		ManagerUserId:  actor.GetUserId(),
		TargetUserId:   targetUserID,
		BlockedUntilMs: blockedUntilMS,
		Reason:         strings.TrimSpace(body.Reason),
	})
	if err != nil {
		httpkit.WriteRPCError(writer, request, err)
		return
	}
	httpkit.WriteOK(writer, request, managerBlockFromProto(resp.GetBlock()))
}

func (h *Handler) unblockManagerBlock(writer http.ResponseWriter, request *http.Request) {
	if h.userProfileClient == nil || h.userHostClient == nil {
		httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
		return
	}
	actor, ok := h.requireManagerCenterActor(writer, request)
	if !ok {
		return
	}
	var body struct {
		Reason string `json:"reason"`
	}
	if request.Body != nil && request.ContentLength != 0 {
		if !httpkit.Decode(writer, request, &body) {
			return
		}
	}
	blockID := strings.TrimSpace(request.PathValue("block_id"))
	if blockID == "" {
		httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
		return
	}
	resp, err := h.userProfileClient.UnblockManagerUser(request.Context(), &userv1.UnblockManagerUserRequest{
		Meta:          httpkit.UserMeta(request, ""),
		ManagerUserId: actor.GetUserId(),
		BlockId:       blockID,
		Reason:        strings.TrimSpace(body.Reason),
	})
	if err != nil {
		httpkit.WriteRPCError(writer, request, err)
		return
	}
	httpkit.WriteOK(writer, request, managerBlockFromProto(resp.GetBlock()))
}

func (h *Handler) setManagerUserLevel(writer http.ResponseWriter, request *http.Request) {
	if h.growthLevelClient == nil || h.userProfileClient == nil || h.userHostClient == nil {
		httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
		return
	}
	var body struct {
		CommandID       string          `json:"command_id"`
		CommandIDAlt    string          `json:"commandId"`
		TargetUserID    json.RawMessage `json:"target_user_id"`
		TargetUserIDAlt json.RawMessage `json:"targetUserId"`
		Track           string          `json:"track"`
		Level           int32           `json:"level"`
		Reason          string          `json:"reason"`
	}
	if !httpkit.Decode(writer, request, &body) {
		return
	}
	commandID := strings.TrimSpace(body.CommandID)
	if commandID == "" {
		commandID = strings.TrimSpace(body.CommandIDAlt)
	}
	targetUserID, targetOK := rawInt64(body.TargetUserID, body.TargetUserIDAlt)
	track := strings.ToLower(strings.TrimSpace(body.Track))
	if commandID == "" || !targetOK || targetUserID <= 0 || !validManagerLevelTrack(track) || body.Level < 1 || body.Level > 11 {
		httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
		return
	}
	if !h.requireManagerCapability(writer, request, managerUpdateUserLevelCapability) {
		return
	}
	// 等级设置会把目标轨道 total_value 直接改到对应规则阈值，并由 activity-service 补齐 1..目标等级奖励。
	if _, ok := h.requireManagerTarget(writer, request, targetUserID); !ok {
		return
	}
	resp, err := h.growthLevelClient.SetUserLevel(request.Context(), &activityv1.SetUserLevelRequest{
		Meta:           httpkit.ActivityMeta(request),
		CommandId:      commandID,
		UserId:         targetUserID,
		Track:          track,
		Level:          body.Level,
		OperatorUserId: auth.UserIDFromContext(request.Context()),
		Reason:         strings.TrimSpace(body.Reason),
	})
	if err != nil {
		httpkit.WriteRPCError(writer, request, err)
		return
	}
	httpkit.WriteOK(writer, request, managerLevelUpdateFromProto(resp))
}

func (h *Handler) createManagerBDLeader(writer http.ResponseWriter, request *http.Request) {
	if h.userHostAdminClient == nil || h.userHostClient == nil || h.userProfileClient == nil {
		httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
		return
	}
	var body struct {
		CommandID       string          `json:"command_id"`
		CommandIDAlt    string          `json:"commandId"`
		TargetUserID    json.RawMessage `json:"target_user_id"`
		TargetUserIDAlt json.RawMessage `json:"targetUserId"`
		Reason          string          `json:"reason"`
	}
	if !httpkit.Decode(writer, request, &body) {
		return
	}
	actor, ok := h.requireManagerCenterActor(writer, request)
	if !ok {
		return
	}
	if !h.requireManagerCapability(writer, request, managerAddBDLeaderCapability) {
		return
	}
	commandID := strings.TrimSpace(body.CommandID)
	if commandID == "" {
		commandID = strings.TrimSpace(body.CommandIDAlt)
	}
	targetUserID, targetOK := rawInt64(body.TargetUserID, body.TargetUserIDAlt)
	if commandID == "" || !targetOK || targetUserID <= 0 {
		httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
		return
	}
	// 写入口重新解析策略并读取 active 目标，不能信任搜索结果或客户端自报范围。
	target, ok := h.requireManagerTargetWithActor(writer, request, actor, targetUserID)
	if !ok {
		return
	}
	reason := strings.TrimSpace(body.Reason)
	if reason == "" {
		reason = "manager_center"
	}
	resp, err := h.userHostAdminClient.CreateBDLeader(request.Context(), &userv1.CreateBDLeaderRequest{
		Meta:         httpkit.UserMeta(request, ""),
		CommandId:    commandID,
		AdminUserId:  actor.GetUserId(),
		TargetUserId: targetUserID,
		Reason:       reason,
	})
	if err != nil {
		httpkit.WriteRPCError(writer, request, err)
		return
	}
	httpkit.WriteOK(writer, request, map[string]any{
		"bd_profile":  managerBDLeaderFromProto(resp.GetBdProfile()),
		"target_user": managerUserFromProto(target, nil),
	})
}

func (h *Handler) transferManagerUserCountry(writer http.ResponseWriter, request *http.Request) {
	if h.userProfileClient == nil || h.userHostClient == nil {
		httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
		return
	}
	var body struct {
		TargetUserID    json.RawMessage `json:"target_user_id"`
		TargetUserIDAlt json.RawMessage `json:"targetUserId"`
		Country         string          `json:"country"`
		CountryCode     string          `json:"country_code"`
		CountryCodeAlt  string          `json:"countryCode"`
		Reason          string          `json:"reason"`
	}
	if !httpkit.Decode(writer, request, &body) {
		return
	}
	actor, ok := h.requireManagerCenterActor(writer, request)
	if !ok {
		return
	}
	if !h.requireManagerCapability(writer, request, managerTransferUserCountryCapability) {
		return
	}
	targetUserID, targetOK := rawInt64(body.TargetUserID, body.TargetUserIDAlt)
	country := strings.ToUpper(strings.TrimSpace(body.Country))
	if country == "" {
		country = strings.ToUpper(strings.TrimSpace(body.CountryCode))
	}
	if country == "" {
		country = strings.ToUpper(strings.TrimSpace(body.CountryCodeAlt))
	}
	if !targetOK || targetUserID <= 0 || country == "" {
		httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
		return
	}
	reason := strings.TrimSpace(body.Reason)
	if reason == "" {
		reason = "manager_center_country_transfer"
	}
	// 国家转移也属于经理数据范围；拥有功能权限不代表可以越过 country/region/global 有效范围选择任意目标。
	if _, ok := h.requireManagerTargetWithActor(writer, request, actor, targetUserID); !ok {
		return
	}
	resp, err := h.userProfileClient.AdminChangeUserCountry(request.Context(), &userv1.AdminChangeUserCountryRequest{
		Meta:         httpkit.UserMeta(request, ""),
		TargetUserId: targetUserID,
		Country:      country,
		AdminUserId:  actor.GetUserId(),
		Reason:       reason,
	})
	if err != nil {
		httpkit.WriteRPCError(writer, request, err)
		return
	}
	// 经理入口和 App/后台入口必须保持国家变更副作用一致；房间迁移由 user_outbox 保证，旧区域 IM 群清理在提交后尽力执行。
	h.removeManagerOldRegionBroadcastMemberBestEffort(request, targetUserID, resp.GetOldRegionId(), resp.GetNewRegionId())
	httpkit.WriteOK(writer, request, map[string]any{
		"user":           managerUserFromProto(resp.GetUser(), nil),
		"old_region_id":  resp.GetOldRegionId(),
		"new_region_id":  resp.GetNewRegionId(),
		"region_changed": resp.GetRegionChanged(),
	})
}

func (h *Handler) requireManagerResourceGrantCapability(writer http.ResponseWriter, request *http.Request) bool {
	return h.requireManagerCapability(writer, request, managerResourceGrantCapability)
}

func managerSearchScene(request *http.Request) string {
	switch strings.ToLower(strings.TrimSpace(request.URL.Query().Get("action"))) {
	case "send-vip":
		return "manager_vip_grant"
	case "update-user-level":
		return "manager_update_user_level"
	case "add-bd-leader":
		return "manager_add_bd_leader"
	case "add-admin":
		return "manager_add_admin"
	case "add-superadmin":
		return "manager_add_superadmin"
	case "block-user":
		return "manager_block_user"
	case "transfer-user-country":
		return "manager_transfer_user_country"
	default:
		return "manager_resource_grant"
	}
}

func (h *Handler) managerCurrentVIPByUser(request *http.Request, userIDs []int64, scene string) map[int64]*managerVipData {
	if scene != "manager_vip_grant" || h.walletClient == nil || len(userIDs) == 0 {
		return nil
	}
	result := make(map[int64]*managerVipData, len(userIDs))
	for _, userID := range userIDs {
		// VIP 状态是赠送弹窗的辅助展示；查询失败不阻断查人，最终赠送仍由 wallet-service 在事务内复验等级。
		resp, err := h.walletClient.GetMyVip(request.Context(), &walletv1.GetMyVipRequest{
			RequestId: httpkit.RequestIDFromContext(request.Context()),
			AppCode:   appcode.FromContext(request.Context()),
			UserId:    userID,
		})
		if err != nil {
			logx.Warn(request.Context(), "manager_vip_status_lookup_failed", slog.Int64("target_user_id", userID))
			continue
		}
		result[userID] = managerVipFromProto(resp.GetVip())
	}
	return result
}

func (h *Handler) requireManagerCenterActor(writer http.ResponseWriter, request *http.Request) (*userv1.User, bool) {
	if !h.requireManagerCapability(writer, request, managerCenterCapability) {
		return nil, false
	}
	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 nil, false
	}
	actor := resp.GetUser()
	if actor == nil || actor.GetStatus() != userv1.UserStatus_USER_STATUS_ACTIVE {
		httpkit.WriteError(writer, request, http.StatusForbidden, httpkit.CodePermissionDenied, "permission denied")
		return nil, false
	}
	return actor, true
}

func (h *Handler) requireManagerCapability(writer http.ResponseWriter, request *http.Request, capability string) bool {
	resp, err := h.userHostClient.CheckBusinessCapability(request.Context(), &userv1.CheckBusinessCapabilityRequest{
		Meta:        httpkit.UserMeta(request, ""),
		ActorUserId: auth.UserIDFromContext(request.Context()),
		Capability:  capability,
	})
	if err != nil {
		httpkit.WriteRPCError(writer, request, err)
		return false
	}
	if !resp.GetAllowed() {
		httpkit.WriteError(writer, request, http.StatusForbidden, httpkit.CodePermissionDenied, "permission denied")
		return false
	}
	return true
}

func (h *Handler) managerEffectiveScope(writer http.ResponseWriter, request *http.Request) (string, bool) {
	resp, err := h.userHostClient.GetRoleScopePolicy(request.Context(), &userv1.GetRoleScopePolicyRequest{
		Meta:  httpkit.UserMeta(request, ""),
		Scene: managerOperationScopeScene,
	})
	if err != nil {
		httpkit.WriteRPCError(writer, request, err)
		return "", false
	}
	policy := resp.GetPolicy()
	if policy == nil || !validManagerRoleScope(policy.GetBaseScope()) {
		httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
		return "", false
	}
	scope := policy.GetBaseScope()
	if !policy.GetRegionExpansionConfigurable() {
		return scope, true
	}
	expandedScope := policy.GetExpandedScope()
	if expandedScope == "" {
		// 兼容上一版 user-service；旧协议只支持扩到同区域，滚动升级期间保持旧边界而不是放大到 global。
		expandedScope = managerRoleScopeRegion
	}
	if !validManagerRoleScope(expandedScope) {
		httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
		return "", false
	}
	capability, err := h.userHostClient.CheckBusinessCapability(request.Context(), &userv1.CheckBusinessCapabilityRequest{
		Meta:        httpkit.UserMeta(request, ""),
		ActorUserId: auth.UserIDFromContext(request.Context()),
		Capability:  managerCrossRegionCapability,
	})
	if err != nil {
		httpkit.WriteRPCError(writer, request, err)
		return "", false
	}
	if capability.GetAllowed() {
		scope = expandedScope
	}
	return scope, true
}

func (h *Handler) requireManagerTarget(writer http.ResponseWriter, request *http.Request, targetUserID int64) (*userv1.User, bool) {
	actor, ok := h.requireManagerCenterActor(writer, request)
	if !ok {
		return nil, false
	}
	return h.requireManagerTargetWithActor(writer, request, actor, targetUserID)
}

func (h *Handler) requireManagerTargetWithActor(writer http.ResponseWriter, request *http.Request, actor *userv1.User, targetUserID int64) (*userv1.User, bool) {
	scope, ok := h.managerEffectiveScope(writer, request)
	if !ok {
		return nil, false
	}
	target, ok := h.requireActiveTarget(writer, request, targetUserID)
	if !ok {
		return nil, false
	}
	if !managerTargetMatchesScope(scope, actor, target) {
		httpkit.WriteError(writer, request, http.StatusForbidden, httpkit.CodePermissionDenied, "permission denied")
		return nil, false
	}
	return target, true
}

// requireActiveTarget 是经理写入口共同的目标用户真实性门禁；范围策略只能决定后续国家/区域匹配，不能绕过 active 用户校验。
func (h *Handler) requireActiveTarget(writer http.ResponseWriter, request *http.Request, targetUserID int64) (*userv1.User, bool) {
	targetResp, err := h.userProfileClient.GetUser(request.Context(), &userv1.GetUserRequest{
		Meta:   httpkit.UserMeta(request, ""),
		UserId: targetUserID,
	})
	if err != nil {
		httpkit.WriteRPCError(writer, request, err)
		return nil, false
	}
	target := targetResp.GetUser()
	if target == nil || target.GetStatus() != userv1.UserStatus_USER_STATUS_ACTIVE {
		httpkit.WriteError(writer, request, http.StatusNotFound, httpkit.CodeNotFound, "not found")
		return nil, false
	}
	return target, true
}

func validManagerRoleScope(scope string) bool {
	switch scope {
	case managerRoleScopeCountry, managerRoleScopeRegion, managerRoleScopeGlobal:
		return true
	default:
		return false
	}
}

// managerTargetMatchesScope 是搜索和写入口共享的唯一匹配器。region 表示在同国家基础上扩到同区域，
// 因而国家映射短暂异常时，打开扩区开关也不会反而失去原本可邀请的同国家用户。
func managerTargetMatchesScope(scope string, actor *userv1.User, target *userv1.User) bool {
	if actor == nil || target == nil {
		return false
	}
	switch scope {
	case managerRoleScopeGlobal:
		return true
	case managerRoleScopeRegion:
		return sameCountry(actor.GetCountry(), target.GetCountry()) || sameRegion(actor.GetRegionId(), target.GetRegionId())
	case managerRoleScopeCountry:
		return sameCountry(actor.GetCountry(), target.GetCountry())
	default:
		return false
	}
}

func (h *Handler) managerPermissionOverview(request *http.Request) map[string]bool {
	permissions := map[string]bool{
		"can_grant_avatar_frame":    false,
		"can_grant_vehicle":         false,
		"can_grant_badge":           false,
		"can_grant_vip":             false,
		"can_update_user_level":     false,
		"can_add_bd_leader":         false,
		"can_manage_cross_region":   false,
		"can_add_admin":             false,
		"can_add_superadmin":        false,
		"can_block_user":            false,
		"can_transfer_user_country": false,
	}
	for key, capability := range map[string]string{
		"can_grant_avatar_frame":    managerGrantAvatarFrameCapability,
		"can_grant_vehicle":         managerGrantVehicleCapability,
		"can_grant_badge":           managerGrantBadgeCapability,
		"can_grant_vip":             managerGrantVIPCapability,
		"can_update_user_level":     managerUpdateUserLevelCapability,
		"can_add_bd_leader":         managerAddBDLeaderCapability,
		"can_manage_cross_region":   managerCrossRegionCapability,
		"can_add_admin":             managerAddAdminCapability,
		"can_add_superadmin":        managerAddSuperadminCapability,
		"can_block_user":            managerBlockUserCapability,
		"can_transfer_user_country": managerTransferUserCountryCapability,
	} {
		resp, err := h.userHostClient.CheckBusinessCapability(request.Context(), &userv1.CheckBusinessCapabilityRequest{
			Meta:        httpkit.UserMeta(request, ""),
			ActorUserId: auth.UserIDFromContext(request.Context()),
			Capability:  capability,
		})
		if err != nil {
			// overview 已经通过 manager_center 身份校验；单项权限读取失败时按关闭返回，避免 H5 展示不可确认的高危入口。
			permissions[key] = false
			continue
		}
		permissions[key] = resp.GetAllowed()
	}
	return permissions
}

func managerCapabilityForResourceType(resourceType string) string {
	switch strings.ToLower(strings.TrimSpace(resourceType)) {
	case "avatar_frame":
		return managerGrantAvatarFrameCapability
	case "vehicle":
		return managerGrantVehicleCapability
	case "badge":
		return managerGrantBadgeCapability
	default:
		return managerResourceGrantCapability
	}
}

func sameCountry(left string, right string) bool {
	return strings.EqualFold(strings.TrimSpace(left), strings.TrimSpace(right)) && strings.TrimSpace(left) != ""
}

func sameRegion(left int64, right int64) bool {
	return left > 0 && left == right
}

func managerGrantResourceFromProto(resource *walletv1.Resource) managerGrantResourceData {
	if resource == nil {
		return managerGrantResourceData{}
	}
	return managerGrantResourceData{
		ResourceID:        httpkit.UserIDString(resource.GetResourceId()),
		ResourceType:      resource.GetResourceType(),
		Name:              resource.GetName(),
		AssetURL:          resource.GetAssetUrl(),
		PreviewURL:        resource.GetPreviewUrl(),
		WalletAssetType:   resource.GetWalletAssetType(),
		WalletAssetAmount: resource.GetWalletAssetAmount(),
		SortOrder:         resource.GetSortOrder(),
	}
}

func managerUserFromProto(user *userv1.User, levels map[string]managerLevelData) managerUserData {
	if user == nil {
		return managerUserData{Levels: managerDefaultLevels()}
	}
	if levels == nil {
		levels = managerDefaultLevels()
	}
	return managerUserData{
		UserID:        httpkit.UserIDString(user.GetUserId()),
		DisplayUserID: user.GetDisplayUserId(),
		Username:      user.GetUsername(),
		Avatar:        user.GetAvatar(),
		Status:        userStatusString(user.GetStatus()),
		Country:       user.GetCountry(),
		RegionID:      user.GetRegionId(),
		RegionCode:    user.GetRegionCode(),
		RegionName:    user.GetRegionName(),
		Levels:        levels,
	}
}

func (h *Handler) managerLevelsByUser(request *http.Request, userIDs []int64) map[int64]map[string]managerLevelData {
	result := map[int64]map[string]managerLevelData{}
	for _, userID := range userIDs {
		result[userID] = managerDefaultLevels()
	}
	if h.growthLevelClient == nil || len(userIDs) == 0 {
		return result
	}
	resp, err := h.growthLevelClient.BatchGetUserLevelDisplayProfiles(request.Context(), &activityv1.BatchGetUserLevelDisplayProfilesRequest{
		Meta:    httpkit.ActivityMeta(request),
		UserIds: userIDs,
	})
	if err != nil {
		return result
	}
	for _, profile := range resp.GetProfiles() {
		if profile.GetUserId() <= 0 {
			continue
		}
		result[profile.GetUserId()] = map[string]managerLevelData{
			"wealth": managerLevelFromProto(profile.GetWealth()),
			"charm":  managerLevelFromProto(profile.GetCharm()),
			"game":   managerLevelFromProto(profile.GetGame()),
		}
	}
	return result
}

func managerDefaultLevels() map[string]managerLevelData {
	return map[string]managerLevelData{
		"wealth": {Track: "wealth"},
		"charm":  {Track: "charm"},
		"game":   {Track: "game"},
	}
}

func managerLevelFromProto(track *activityv1.LevelDisplayTrackProfile) managerLevelData {
	if track == nil {
		return managerLevelData{}
	}
	return managerLevelData{
		Track:  track.GetTrack(),
		Level:  track.GetLevel(),
		TierID: httpkit.UserIDString(track.GetTierId()),
	}
}

func managerBlockFromProto(block *userv1.ManagerUserBlock) managerBlockData {
	if block == nil {
		return managerBlockData{}
	}
	return managerBlockData{
		BlockID:             block.GetBlockId(),
		TargetUserID:        httpkit.UserIDString(block.GetTargetUserId()),
		TargetDisplayUserID: block.GetTargetDisplayUserId(),
		TargetUsername:      block.GetTargetUsername(),
		TargetAvatar:        block.GetTargetAvatar(),
		Country:             block.GetCountry(),
		BlockedUntilMS:      block.GetBlockedUntilMs(),
		Status:              block.GetStatus(),
		Reason:              block.GetReason(),
		CreatedAtMS:         block.GetCreatedAtMs(),
		UpdatedAtMS:         block.GetUpdatedAtMs(),
	}
}

func managerLevelUpdateFromProto(resp *activityv1.SetUserLevelResponse) managerLevelUpdateData {
	if resp == nil {
		return managerLevelUpdateData{}
	}
	return managerLevelUpdateData{
		EventID:        resp.GetEventId(),
		Status:         resp.GetStatus(),
		Track:          resp.GetTrack(),
		PreviousLevel:  resp.GetPreviousLevel(),
		NewLevel:       resp.GetNewLevel(),
		TotalValue:     resp.GetTotalValue(),
		RewardJobCount: resp.GetRewardJobCount(),
		ServerTimeMS:   resp.GetServerTimeMs(),
	}
}

func managerBDLeaderFromProto(profile *userv1.BDProfile) managerBDLeaderData {
	if profile == nil {
		return managerBDLeaderData{}
	}
	return managerBDLeaderData{
		UserID:             httpkit.UserIDString(profile.GetUserId()),
		Role:               profile.GetRole(),
		RegionID:           profile.GetRegionId(),
		ParentLeaderUserID: httpkit.UserIDString(profile.GetParentLeaderUserId()),
		Status:             profile.GetStatus(),
		CreatedByUserID:    httpkit.UserIDString(profile.GetCreatedByUserId()),
		CreatedAtMS:        profile.GetCreatedAtMs(),
		UpdatedAtMS:        profile.GetUpdatedAtMs(),
	}
}

func validManagerLevelTrack(track string) bool {
	switch track {
	case "wealth", "charm", "game":
		return true
	default:
		return false
	}
}

func managerGrantDurationMS(primary *int64, alternate *int64) (int64, bool) {
	// 旧 H5 还没有 duration 字段，默认 7 天保证入口兼容；新 H5 只能在 7/30/90 天之间选择。
	if primary == nil && alternate == nil {
		return managerGrantDefaultDurationMS, true
	}
	if primary != nil && alternate != nil && *primary != *alternate {
		return 0, false
	}
	value := int64(0)
	if primary != nil {
		value = *primary
	} else {
		value = *alternate
	}
	switch value {
	case 7 * managerGrantDayMS, 30 * managerGrantDayMS, 90 * managerGrantDayMS:
		return value, true
	default:
		return 0, false
	}
}

func businessUserLookupFromProto(user *userv1.BusinessUserLookupItem) businessUserLookupData {
	if user == nil {
		return businessUserLookupData{}
	}
	return businessUserLookupData{
		UserID:        httpkit.UserIDString(user.GetUserId()),
		DisplayUserID: user.GetDisplayUserId(),
		Username:      user.GetUsername(),
		Avatar:        user.GetAvatar(),
		Status:        userStatusString(user.GetStatus()),
		RegionID:      user.GetRegionId(),
		RegionCode:    user.GetRegionCode(),
		RegionName:    user.GetRegionName(),
	}
}

func managerVipFromProto(vip *walletv1.UserVip) *managerVipData {
	if vip == nil {
		return &managerVipData{}
	}
	return &managerVipData{
		Level:       vip.GetLevel(),
		Name:        vip.GetName(),
		Active:      vip.GetActive(),
		StartedAtMS: vip.GetStartedAtMs(),
		ExpiresAtMS: vip.GetExpiresAtMs(),
		UpdatedAtMS: vip.GetUpdatedAtMs(),
	}
}

func managerResourceGrantFromProto(grant *walletv1.ResourceGrant) managerResourceGrantData {
	if grant == nil {
		return managerResourceGrantData{}
	}
	resourceID := grant.GetGrantSubjectId()
	if len(grant.GetItems()) > 0 && grant.GetItems()[0].GetResourceId() > 0 {
		resourceID = httpkit.UserIDString(grant.GetItems()[0].GetResourceId())
	}
	return managerResourceGrantData{
		GrantID:        grant.GetGrantId(),
		CommandID:      grant.GetCommandId(),
		TargetUserID:   httpkit.UserIDString(grant.GetTargetUserId()),
		ResourceID:     resourceID,
		Status:         grant.GetStatus(),
		GrantSource:    grant.GetGrantSource(),
		OperatorUserID: httpkit.UserIDString(grant.GetOperatorUserId()),
		CreatedAtMS:    grant.GetCreatedAtMs(),
	}
}

func managerPage(request *http.Request) (int32, int32, bool) {
	page, ok := httpkit.PositiveInt32Query(request, "page", 1)
	if !ok {
		return 0, 0, false
	}
	pageSize, ok := httpkit.PositiveInt32Query(request, "page_size", 20)
	if !ok {
		return 0, 0, false
	}
	if raw := strings.TrimSpace(request.URL.Query().Get("pageSize")); raw != "" {
		parsed, err := strconv.Atoi(raw)
		if err != nil || parsed <= 0 {
			return 0, 0, false
		}
		pageSize = int32(parsed)
	}
	if pageSize > 20 {
		pageSize = 20
	}
	return page, pageSize, true
}

func rawInt64(values ...json.RawMessage) (int64, bool) {
	for _, raw := range values {
		value, ok := parseRawInt64(raw)
		if ok {
			return value, true
		}
	}
	return 0, false
}

func rawInt64List(values ...[]json.RawMessage) ([]int64, bool, bool) {
	for _, raws := range values {
		if raws == nil {
			continue
		}
		if len(raws) == 0 {
			return nil, true, false
		}
		ids := make([]int64, 0, len(raws))
		seen := make(map[int64]struct{}, len(raws))
		for _, raw := range raws {
			value, ok := parseRawInt64(raw)
			if !ok || value <= 0 {
				return nil, true, false
			}
			if _, exists := seen[value]; exists {
				continue
			}
			seen[value] = struct{}{}
			ids = append(ids, value)
		}
		return ids, true, len(ids) > 0
	}
	return nil, false, false
}

func parseRawInt64(raw json.RawMessage) (int64, bool) {
	text := strings.TrimSpace(string(raw))
	if text == "" || text == "null" {
		return 0, false
	}
	if strings.HasPrefix(text, `"`) {
		var value string
		if err := json.Unmarshal(raw, &value); err != nil {
			return 0, false
		}
		text = strings.TrimSpace(value)
	}
	value, err := strconv.ParseInt(text, 10, 64)
	return value, err == nil
}

func userStatusString(status userv1.UserStatus) string {
	switch status {
	case userv1.UserStatus_USER_STATUS_ACTIVE:
		return "active"
	case userv1.UserStatus_USER_STATUS_DISABLED:
		return "disabled"
	case userv1.UserStatus_USER_STATUS_BANNED:
		return "banned"
	default:
		return "unspecified"
	}
}
