package activityapi

import (
	"encoding/json"
	"errors"
	"net/http"
	"strconv"
	"strings"
	"time"

	"hyapp/pkg/appcode"
	"hyapp/pkg/userleaderboard"
	"hyapp/services/gateway-service/internal/appconfig"
	"hyapp/services/gateway-service/internal/auth"
	"hyapp/services/gateway-service/internal/transport/http/httpkit"

	roomv1 "hyapp.local/api/proto/room/v1"
	userv1 "hyapp.local/api/proto/user/v1"
	walletv1 "hyapp.local/api/proto/wallet/v1"
)

const (
	leaderboardTypeSent     = userleaderboard.BoardSent
	leaderboardTypeReceived = userleaderboard.BoardReceived
	leaderboardTypeGame     = userleaderboard.BoardGame
	leaderboardTypeRoom     = "room"

	leaderboardPeriodToday = userleaderboard.PeriodToday
	leaderboardPeriodWeek  = userleaderboard.PeriodWeek
	leaderboardPeriodMonth = userleaderboard.PeriodMonth

	userLeaderboardMaxPageSize = 100
)

type userLeaderboardResponse struct {
	Items        []userLeaderboardItem `json:"items"`
	Total        int64                 `json:"total"`
	Page         int32                 `json:"page"`
	PageSize     int32                 `json:"page_size"`
	BoardType    string                `json:"board_type"`
	Period       string                `json:"period"`
	StartAtMS    int64                 `json:"start_at_ms"`
	EndAtMS      int64                 `json:"end_at_ms"`
	ServerTimeMS int64                 `json:"server_time_ms"`
	RegionID     int64                 `json:"region_id,omitempty"`
	MyRank       *userLeaderboardItem  `json:"my_rank,omitempty"`
}

type userLeaderboardItem struct {
	Rank             int64                `json:"rank"`
	UserID           string               `json:"user_id,omitempty"`
	IdentityHidden   bool                 `json:"identity_hidden,omitempty"`
	RoomID           string               `json:"room_id,omitempty"`
	GiftValue        int64                `json:"gift_value"`
	GiftCount        int64                `json:"gift_count"`
	TransactionCount int64                `json:"transaction_count"`
	LastGiftAtMS     int64                `json:"last_gift_at_ms"`
	User             *userLeaderboardUser `json:"user,omitempty"`
	Room             *userLeaderboardRoom `json:"room,omitempty"`
}

type userLeaderboardUser struct {
	UserID              string   `json:"user_id"`
	DisplayUserID       string   `json:"display_user_id,omitempty"`
	Username            string   `json:"username,omitempty"`
	Avatar              string   `json:"avatar,omitempty"`
	ShortBadgeCoverURLs []string `json:"short_badge_cover_urls,omitempty"`
}

type userLeaderboardRoom struct {
	RoomID      string `json:"room_id"`
	RoomShortID string `json:"room_short_id,omitempty"`
	Title       string `json:"title,omitempty"`
	CoverURL    string `json:"cover_url,omitempty"`
	RoomAvatar  string `json:"room_avatar,omitempty"`
}

func (h *Handler) listUserLeaderboards(writer http.ResponseWriter, request *http.Request) {
	boardType := normalizeUserLeaderboardType(request.URL.Query().Get("board_type"))
	if boardType == "" {
		httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
		return
	}
	period := normalizeUserLeaderboardPeriod(request.URL.Query().Get("period"))
	if period == "" {
		httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
		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
	}
	if pageSize > userLeaderboardMaxPageSize {
		pageSize = userLeaderboardMaxPageSize
	}
	app := appcode.FromContext(request.Context())
	viewerUserID := auth.UserIDFromContext(request.Context())
	regionID := int64(0)
	leaderboardScope := appconfig.ActivityLeaderboardScopeAppGlobal
	if h.runtimeCapabilities != nil {
		capabilities, err := h.runtimeCapabilities.GetRuntimeCapabilities(request.Context(), app)
		if err != nil {
			// 配置读取失败时不能擅自从区域榜切到全 App 榜，否则会在后台库故障期间扩大数据可见范围。
			httpkit.WriteError(writer, request, http.StatusServiceUnavailable, httpkit.CodeUpstreamError, "upstream service error")
			return
		}
		leaderboardScope = capabilities.ActivityLeaderboardScope
	}
	if leaderboardScope != appconfig.ActivityLeaderboardScopeAppGlobal && leaderboardScope != appconfig.ActivityLeaderboardScopeViewerRegion {
		httpkit.WriteError(writer, request, http.StatusServiceUnavailable, httpkit.CodeUpstreamError, "upstream service error")
		return
	}
	if leaderboardScope == appconfig.ActivityLeaderboardScopeViewerRegion {
		if h.userProfileClient == nil {
			httpkit.WriteError(writer, request, http.StatusServiceUnavailable, httpkit.CodeUpstreamError, "upstream service error")
			return
		}
		// 区域只能取 user-service 当前主数据；不接受 query/header 自报 region_id，避免用户跨区读榜。
		profileResp, err := h.userProfileClient.GetUser(request.Context(), &userv1.GetUserRequest{
			Meta:   httpkit.UserMeta(request, ""),
			UserId: viewerUserID,
		})
		if err != nil {
			httpkit.WriteRPCError(writer, request, err)
			return
		}
		profile := profileResp.GetUser()
		if profile == nil || profile.GetRegionId() <= 0 {
			httpkit.WriteError(writer, request, http.StatusForbidden, httpkit.CodeProfileRequired, "profile required")
			return
		}
		if profile.GetAppCode() != "" && appcode.Normalize(profile.GetAppCode()) != app {
			httpkit.WriteError(writer, request, http.StatusForbidden, httpkit.CodePermissionDenied, "permission denied")
			return
		}
		regionID = profile.GetRegionId()
	}
	if boardType == leaderboardTypeRoom {
		h.listRoomGiftLeaderboards(writer, request, period, page, pageSize, regionID)
		return
	}
	if h.userLeaderboard == nil {
		httpkit.WriteError(writer, request, http.StatusServiceUnavailable, httpkit.CodeUpstreamError, "upstream service error")
		return
	}

	now := time.Now().UTC()
	query := userleaderboard.Query{
		AppCode:   app,
		RegionID:  regionID,
		BoardType: boardType,
		Period:    period,
		Page:      int(page),
		PageSize:  int(pageSize),
		Now:       now,
	}
	result, err := h.userLeaderboard.List(request.Context(), query)
	if err != nil {
		httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
		return
	}
	items := userLeaderboardItemsFromRedisEntries(result.Items)

	var myRank *userLeaderboardItem
	if viewerUserID > 0 && boardType != leaderboardTypeRoom {
		entry, found, err := h.userLeaderboard.RankForUser(request.Context(), query, viewerUserID)
		if err != nil {
			httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
			return
		}
		if found {
			item := userLeaderboardItemFromRedisEntry(entry)
			myRank = &item
		}
	}
	if err := h.enrichUserLeaderboardItems(request, boardType, items, myRank); err != nil {
		httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
		return
	}

	httpkit.WriteOK(writer, request, userLeaderboardResponse{
		Items:        items,
		Total:        result.Total,
		Page:         page,
		PageSize:     pageSize,
		BoardType:    result.BoardType,
		Period:       result.Period,
		StartAtMS:    result.StartAtMS,
		EndAtMS:      result.EndAtMS,
		ServerTimeMS: result.ServerTimeMS,
		RegionID:     regionID,
		MyRank:       myRank,
	})
}

func (h *Handler) listRoomGiftLeaderboards(writer http.ResponseWriter, request *http.Request, period string, page int32, pageSize int32, regionID int64) {
	if h.roomQueryClient == nil {
		httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
		return
	}
	resp, err := h.roomQueryClient.ListRoomGiftLeaderboard(request.Context(), &roomv1.ListRoomGiftLeaderboardRequest{
		Meta:     httpkit.RoomMeta(request, "", ""),
		Period:   period,
		Page:     page,
		PageSize: pageSize,
		RegionId: regionID,
	})
	if err != nil {
		httpkit.WriteRPCError(writer, request, err)
		return
	}
	httpkit.WriteOK(writer, request, userLeaderboardResponse{
		Items:        userLeaderboardItemsFromRoomGiftLeaderboard(resp.GetItems()),
		Total:        resp.GetTotal(),
		Page:         page,
		PageSize:     pageSize,
		BoardType:    leaderboardTypeRoom,
		Period:       resp.GetPeriod(),
		StartAtMS:    resp.GetStartAtMs(),
		EndAtMS:      resp.GetEndAtMs(),
		ServerTimeMS: resp.GetServerTimeMs(),
		RegionID:     regionID,
	})
}

func normalizeUserLeaderboardType(raw string) string {
	switch strings.ToLower(strings.TrimSpace(raw)) {
	case "room", "rooms", "room_gift", "room_gifts":
		return leaderboardTypeRoom
	default:
		return userleaderboard.NormalizeBoardType(raw)
	}
}

func normalizeUserLeaderboardPeriod(raw string) string {
	return userleaderboard.NormalizePeriod(raw)
}

func userLeaderboardPeriodWindow(period string, now time.Time) (time.Time, time.Time) {
	return userleaderboard.QueryWindow(period, now)
}

func userLeaderboardItemsFromRedisEntries(entries []userleaderboard.Entry) []userLeaderboardItem {
	out := make([]userLeaderboardItem, 0, len(entries))
	for _, entry := range entries {
		out = append(out, userLeaderboardItemFromRedisEntry(entry))
	}
	return out
}

func userLeaderboardItemFromRedisEntry(entry userleaderboard.Entry) userLeaderboardItem {
	return userLeaderboardItem{
		Rank:             entry.Rank,
		UserID:           entry.UserID,
		GiftValue:        entry.GiftValue,
		GiftCount:        entry.GiftCount,
		TransactionCount: entry.TransactionCount,
		LastGiftAtMS:     entry.LastGiftAtMS,
		User:             &userLeaderboardUser{UserID: entry.UserID},
	}
}

func (h *Handler) enrichUserLeaderboardItems(request *http.Request, boardType string, items []userLeaderboardItem, myRank *userLeaderboardItem) error {
	if boardType == leaderboardTypeRoom {
		// room 榜已经由 room-service 的 zset + room_list_entries 返回卡片，不再读取完整 RoomSnapshot。
		return nil
	}
	userIDs := make([]int64, 0, len(items)+1)
	for _, item := range items {
		if id, err := strconv.ParseInt(item.UserID, 10, 64); err == nil && id > 0 {
			userIDs = append(userIDs, id)
		}
	}
	if myRank != nil {
		if id, err := strconv.ParseInt(myRank.UserID, 10, 64); err == nil && id > 0 {
			userIDs = append(userIDs, id)
		}
	}
	profiles, err := h.userLeaderboardProfiles(request, userIDs)
	if err != nil {
		return err
	}
	presentation, err := h.userLeaderboardPresentation(request, userIDs)
	if err != nil {
		return err
	}
	for i := range items {
		applyUserLeaderboardProfile(&items[i], profiles)
		applyUserLeaderboardShortBadgeCovers(&items[i], presentation.ShortBadgeCovers)
		applyUserLeaderboardPrivacy(&items[i], presentation.LeaderboardInvisible)
	}
	if myRank != nil {
		applyUserLeaderboardProfile(myRank, profiles)
		applyUserLeaderboardShortBadgeCovers(myRank, presentation.ShortBadgeCovers)
		applyUserLeaderboardPrivacy(myRank, presentation.LeaderboardInvisible)
	}
	return nil
}

func userLeaderboardItemsFromRoomGiftLeaderboard(items []*roomv1.RoomGiftLeaderboardItem) []userLeaderboardItem {
	out := make([]userLeaderboardItem, 0, len(items))
	for _, item := range items {
		if item == nil {
			continue
		}
		roomID := item.GetRoomId()
		room := userLeaderboardRoomFromListItem(item.GetRoom())
		if room == nil {
			room = &userLeaderboardRoom{RoomID: roomID}
		}
		out = append(out, userLeaderboardItem{
			Rank:      item.GetRank(),
			RoomID:    roomID,
			GiftValue: item.GetCoinSpent(),
			Room:      room,
		})
	}
	return out
}

func userLeaderboardRoomFromListItem(room *roomv1.RoomListItem) *userLeaderboardRoom {
	if room == nil {
		return nil
	}
	roomID := room.GetRoomId()
	if roomID == "" {
		return nil
	}
	return &userLeaderboardRoom{
		RoomID:      roomID,
		RoomShortID: room.GetRoomShortId(),
		Title:       room.GetTitle(),
		CoverURL:    room.GetCoverUrl(),
		RoomAvatar:  room.GetCoverUrl(),
	}
}

func roomLeaderboardRoomFromSnapshot(roomID string, snapshot *roomv1.RoomSnapshot) *userLeaderboardRoom {
	room := &userLeaderboardRoom{RoomID: roomID}
	if snapshot == nil {
		return room
	}
	if snapshot.GetRoomId() != "" {
		room.RoomID = snapshot.GetRoomId()
	}
	ext := snapshot.GetRoomExt()
	room.RoomShortID = snapshot.GetRoomShortId()
	if room.RoomShortID == "" {
		room.RoomShortID = ext["room_short_id"]
	}
	room.Title = ext["title"]
	room.CoverURL = ext["cover_url"]
	room.RoomAvatar = room.CoverURL
	return room
}

func (h *Handler) userLeaderboardProfiles(request *http.Request, userIDs []int64) (map[int64]*userv1.User, error) {
	result := make(map[int64]*userv1.User)
	if h.userProfileClient == nil || len(userIDs) == 0 {
		return result, nil
	}
	userIDs = uniqueUserLeaderboardInt64s(userIDs)
	resp, err := h.userProfileClient.BatchGetUsers(request.Context(), &userv1.BatchGetUsersRequest{
		Meta:    httpkit.UserMeta(request, ""),
		UserIds: userIDs,
	})
	if err != nil {
		return nil, err
	}
	return resp.GetUsers(), nil
}

func applyUserLeaderboardProfile(item *userLeaderboardItem, profiles map[int64]*userv1.User) {
	if item == nil {
		return
	}
	id, err := strconv.ParseInt(item.UserID, 10, 64)
	if err != nil || id <= 0 {
		return
	}
	profile := profiles[id]
	if profile == nil {
		return
	}
	item.User = &userLeaderboardUser{
		UserID:        strconv.FormatInt(profile.GetUserId(), 10),
		DisplayUserID: profile.GetDisplayUserId(),
		Username:      profile.GetUsername(),
		Avatar:        profile.GetAvatar(),
	}
}

type userLeaderboardPresentation struct {
	ShortBadgeCovers     map[int64][]string
	LeaderboardInvisible map[int64]struct{}
}

func (h *Handler) userLeaderboardPresentation(request *http.Request, userIDs []int64) (userLeaderboardPresentation, error) {
	result := userLeaderboardPresentation{
		ShortBadgeCovers:     make(map[int64][]string),
		LeaderboardInvisible: make(map[int64]struct{}),
	}
	userIDs = uniqueUserLeaderboardInt64s(userIDs)
	if len(userIDs) == 0 {
		return result, nil
	}
	if h.walletClient == nil {
		// 隐私权益不能在 wallet 未接入时 fail-open，否则 VIP 用户会在依赖故障期间暴露身份。
		return userLeaderboardPresentation{}, errors.New("wallet client is required for leaderboard privacy")
	}
	// 一页榜单只发起一次批量查询：静态短徽章与最终 VIP 隐私权限使用同一批用户，避免 N+1 放大。
	resp, err := h.walletClient.BatchGetUserEquippedResources(request.Context(), &walletv1.BatchGetUserEquippedResourcesRequest{
		RequestId:       httpkit.RequestIDFromContext(request.Context()),
		AppCode:         appcode.FromContext(request.Context()),
		UserIds:         userIDs,
		ResourceTypes:   []string{"badge"},
		VipBenefitCodes: []string{"leaderboard_invisible"},
	})
	if err != nil {
		return userLeaderboardPresentation{}, err
	}
	return userLeaderboardPresentationFromResponse(resp), nil
}

func userLeaderboardShortBadgeCoversFromResponse(resp *walletv1.BatchGetUserEquippedResourcesResponse) map[int64][]string {
	return userLeaderboardPresentationFromResponse(resp).ShortBadgeCovers
}

func userLeaderboardPresentationFromResponse(resp *walletv1.BatchGetUserEquippedResourcesResponse) userLeaderboardPresentation {
	result := userLeaderboardPresentation{
		ShortBadgeCovers:     make(map[int64][]string),
		LeaderboardInvisible: make(map[int64]struct{}),
	}
	if resp == nil {
		return result
	}
	for _, user := range resp.GetUsers() {
		if user.GetUserId() <= 0 {
			continue
		}
		for _, code := range user.GetAllowedVipBenefitCodes() {
			if strings.EqualFold(strings.TrimSpace(code), "leaderboard_invisible") {
				result.LeaderboardInvisible[user.GetUserId()] = struct{}{}
				break
			}
		}
		seen := make(map[string]struct{})
		for _, entitlement := range user.GetResources() {
			resource := entitlement.GetResource()
			if resource == nil || !isUserLeaderboardShortBadge(resource.GetMetadataJson()) {
				continue
			}
			// preview_url 是静态封面契约；动画 asset_url 不能直接交给 H5 img 渲染。
			coverURL := strings.TrimSpace(resource.GetPreviewUrl())
			if coverURL == "" {
				continue
			}
			if _, exists := seen[coverURL]; exists {
				continue
			}
			seen[coverURL] = struct{}{}
			result.ShortBadgeCovers[user.GetUserId()] = append(result.ShortBadgeCovers[user.GetUserId()], coverURL)
		}
	}
	return result
}

func applyUserLeaderboardPrivacy(item *userLeaderboardItem, invisible map[int64]struct{}) {
	if item == nil {
		return
	}
	userID, err := strconv.ParseInt(item.UserID, 10, 64)
	if err != nil || userID <= 0 {
		return
	}
	if _, hidden := invisible[userID]; !hidden {
		return
	}
	// 名次和分值仍是榜单事实；身份字段必须整体清空，避免 H5 禁止跳转后响应里仍泄露内部 ID 或资料。
	item.UserID = ""
	item.User = nil
	item.IdentityHidden = true
}

func isUserLeaderboardShortBadge(rawMetadata string) bool {
	metadata := map[string]any{}
	if err := json.Unmarshal([]byte(strings.TrimSpace(rawMetadata)), &metadata); err != nil {
		return false
	}
	if strings.EqualFold(strings.TrimSpace(toUserLeaderboardString(metadata["badge_kind"])), "level") {
		// 等级徽章有独立展示位，不能混入用户主动佩戴的普通短徽章。
		return false
	}
	switch strings.ToLower(strings.TrimSpace(toUserLeaderboardString(metadata["badge_form"]))) {
	case "short", "tile", "icon", "medal":
		return true
	default:
		return false
	}
}

func toUserLeaderboardString(value any) string {
	text, _ := value.(string)
	return text
}

func applyUserLeaderboardShortBadgeCovers(item *userLeaderboardItem, covers map[int64][]string) {
	if item == nil || item.User == nil {
		return
	}
	userID, err := strconv.ParseInt(item.UserID, 10, 64)
	if err != nil || userID <= 0 || len(covers[userID]) == 0 {
		return
	}
	item.User.ShortBadgeCoverURLs = append([]string(nil), covers[userID]...)
}

func uniqueUserLeaderboardInt64s(values []int64) []int64 {
	seen := make(map[int64]struct{}, len(values))
	out := make([]int64, 0, len(values))
	for _, value := range values {
		if value <= 0 {
			continue
		}
		if _, ok := seen[value]; ok {
			continue
		}
		seen[value] = struct{}{}
		out = append(out, value)
	}
	return out
}
