package roomapi

import (
	"strconv"

	"hyapp/pkg/imgroup"
	"hyapp/pkg/tencentrtc"
	"hyapp/services/gateway-service/internal/appconfig"

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

const joinRoomContributionRankLimit = 10

type roomListData struct {
	Rooms      []roomListItemData    `json:"rooms"`
	NextCursor string                `json:"next_cursor,omitempty"`
	Countries  []roomListCountryData `json:"countries,omitempty"`
}

type roomListCountryData struct {
	CountryID          int64  `json:"country_id,omitempty"`
	CountryCode        string `json:"country_code"`
	CountryName        string `json:"country_name,omitempty"`
	CountryDisplayName string `json:"country_display_name,omitempty"`
	CountryFlag        string `json:"country_flag,omitempty"`
	SortOrder          int32  `json:"sort_order,omitempty"`
}

type myRoomData struct {
	HasRoom      bool              `json:"has_room"`
	Room         *roomListItemData `json:"room,omitempty"`
	ServerTimeMS int64             `json:"server_time_ms"`
}

type roomListItemData struct {
	RoomID                     string              `json:"room_id"`
	IMGroupID                  string              `json:"im_group_id"`
	OwnerUserID                string              `json:"owner_user_id,omitempty"`
	OwnerVIPLevel              int32               `json:"owner_vip_level"`
	Title                      string              `json:"title,omitempty"`
	CoverURL                   string              `json:"cover_url,omitempty"`
	Mode                       string              `json:"mode,omitempty"`
	Status                     string              `json:"status,omitempty"`
	Locked                     bool                `json:"locked"`
	Heat                       int64               `json:"heat"`
	OnlineCount                int32               `json:"online_count"`
	SeatCount                  int32               `json:"seat_count"`
	OccupiedSeatCount          int32               `json:"occupied_seat_count"`
	VisibleRegionID            int64               `json:"visible_region_id,omitempty"`
	AppCode                    string              `json:"app_code,omitempty"`
	RoomShortID                string              `json:"room_short_id,omitempty"`
	CountryCode                string              `json:"country_code,omitempty"`
	CountryFlag                string              `json:"country_flag,omitempty"`
	RoomBorderURL              string              `json:"room_border_url"`
	RoomBorder                 *roomDecorationData `json:"room_border,omitempty"`
	RoomNameStyle              *roomDecorationData `json:"room_name_style,omitempty"`
	RocketNearCompletion       bool                `json:"rocket_near_completion"`
	HasActiveRedPacket         bool                `json:"has_active_red_packet"`
	ActiveRedPacketExpiresAtMS int64               `json:"active_red_packet_expires_at_ms,omitempty"`
}

type createRoomData struct {
	Result       roomCommandResultData `json:"result"`
	Room         roomInitialData       `json:"room"`
	Seats        []roomSeatData        `json:"seats"`
	IM           roomIMData            `json:"im"`
	ServerTimeMS int64                 `json:"server_time_ms"`
}

type joinRoomData struct {
	Result           roomCommandResultData    `json:"result"`
	Room             roomInitialData          `json:"room"`
	Viewer           roomViewerData           `json:"viewer"`
	Seats            []roomSeatData           `json:"seats"`
	ContributionRank []roomRankItemData       `json:"contribution_rank"`
	Profiles         []roomDisplayProfileData `json:"profiles"`
	IM               roomIMData               `json:"im"`
	RTC              roomRTCData              `json:"rtc"`
	EffectiveVIP     roomEffectiveVIPData     `json:"effective_vip"`
	ServerTimeMS     int64                    `json:"server_time_ms"`
}

type joinRoomLiteData struct {
	Result       roomCommandResultData  `json:"result"`
	Room         roomInitialData        `json:"room"`
	Viewer       roomViewerData         `json:"viewer"`
	Seats        []roomSeatData         `json:"seats"`
	Profiles     []roomBasicProfileData `json:"profiles"`
	IM           roomIMData             `json:"im"`
	RTC          roomRTCData            `json:"rtc"`
	EffectiveVIP roomEffectiveVIPData   `json:"effective_vip"`
	ServerTimeMS int64                  `json:"server_time_ms"`
}

type roomDetailData struct {
	Room         roomInitialData          `json:"room"`
	Viewer       roomViewerData           `json:"viewer"`
	IsFollowed   bool                     `json:"is_followed"`
	Seats        []roomSeatData           `json:"seats"`
	RankTop      []roomRankItemData       `json:"rank_top"`
	OnlineCount  int32                    `json:"online_count"`
	Permissions  roomPermissionsData      `json:"permissions"`
	Profiles     []roomDisplayProfileData `json:"profiles,omitempty"`
	IM           roomIMData               `json:"im"`
	RTC          roomRTCData              `json:"rtc"`
	ServerTimeMS int64                    `json:"server_time_ms"`
}

// roomEffectiveVIPData 是进房瞬间由 room-service 固化的轻量展示事实。
// 完整权益仍以 wallet /vip/me 为准；这里仅用于当前用户首屏和房内进场效果立即渲染。
type roomEffectiveVIPData struct {
	ProgramType            string `json:"program_type"`
	Level                  int32  `json:"level"`
	Name                   string `json:"name"`
	RoomEntryNoticeEnabled bool   `json:"room_entry_notice_enabled"`
}

type roomOnlineUsersData struct {
	Items        []roomOnlineUserData `json:"items"`
	Total        int64                `json:"total"`
	Page         int32                `json:"page"`
	PageSize     int32                `json:"page_size"`
	ServerTimeMS int64                `json:"server_time_ms"`
}

type roomOnlineUserData struct {
	UserID       string                  `json:"user_id"`
	Role         string                  `json:"role,omitempty"`
	IsOwner      bool                    `json:"is_owner"`
	RoomRole     string                  `json:"room_role,omitempty"`
	GiftValue    int64                   `json:"gift_value"`
	JoinedAtMS   int64                   `json:"joined_at_ms,omitempty"`
	LastSeenAtMS int64                   `json:"last_seen_at_ms,omitempty"`
	Profile      *roomDisplayProfileData `json:"profile,omitempty"`
}

type roomBannedUsersData struct {
	RoomID       string               `json:"room_id"`
	Items        []roomBannedUserData `json:"items"`
	Total        int64                `json:"total"`
	Page         int32                `json:"page"`
	PageSize     int32                `json:"page_size"`
	ServerTimeMS int64                `json:"server_time_ms"`
}

type roomBannedUserData struct {
	UserID             string `json:"user_id"`
	DisplayUserID      string `json:"display_user_id"`
	Username           string `json:"username"`
	Avatar             string `json:"avatar"`
	Country            string `json:"country"`
	CountryName        string `json:"country_name"`
	CountryDisplayName string `json:"country_display_name"`
	CountryFlag        string `json:"country_flag"`
	ActorUserID        string `json:"actor_user_id"`
	CreatedAtMS        int64  `json:"created_at_ms"`
	UnbanAtMS          int64  `json:"unban_at_ms"`
	RemainingMS        int64  `json:"remaining_ms"`
	Permanent          bool   `json:"permanent"`
}

type roomContributionRankData struct {
	RoomID           string                         `json:"room_id"`
	Period           string                         `json:"period"`
	StartAtMS        int64                          `json:"start_at_ms"`
	EndAtMS          int64                          `json:"end_at_ms"`
	Total            int64                          `json:"total"`
	ContributionRank []roomContributionRankItemData `json:"contribution_rank"`
	Profiles         []roomDisplayProfileData       `json:"profiles"`
	ServerTimeMS     int64                          `json:"server_time_ms"`
}

type roomContributionRankItemData struct {
	Rank        int64                   `json:"rank"`
	UserID      string                  `json:"user_id"`
	Score       int64                   `json:"score"`
	GiftValue   int64                   `json:"gift_value"`
	UpdatedAtMS int64                   `json:"updated_at_ms,omitempty"`
	Profile     *roomDisplayProfileData `json:"profile,omitempty"`
}

type setRoomAdminData struct {
	Result       roomCommandResultData `json:"result"`
	TargetUserID string                `json:"target_user_id"`
	RoomRole     string                `json:"room_role"`
	ServerTimeMS int64                 `json:"server_time_ms"`
}

type roomPermissionsData struct {
	IsOwner        bool `json:"is_owner"`
	IsAdmin        bool `json:"is_admin"`
	IsMuted        bool `json:"is_muted"`
	CanSpeak       bool `json:"can_speak"`
	CanSendGift    bool `json:"can_send_gift"`
	CanMicUp       bool `json:"can_mic_up"`
	CanManageRoom  bool `json:"can_manage_room"`
	CanLeaveRoom   bool `json:"can_leave_room"`
	CanCloseRoom   bool `json:"can_close_room"`
	CanMuteMic     bool `json:"can_mute_mic"`
	CanOpenGiftBox bool `json:"can_open_gift_box"`
}

type roomDisplayProfileData struct {
	UserID              string           `json:"user_id"`
	Username            string           `json:"username"`
	Avatar              string           `json:"avatar"`
	DisplayUserID       string           `json:"display_user_id"`
	PrettyID            string           `json:"pretty_id,omitempty"`
	PrettyDisplayUserID string           `json:"pretty_display_user_id,omitempty"`
	Gender              string           `json:"gender"`
	Age                 int32            `json:"age"`
	Country             string           `json:"country"`
	CountryName         string           `json:"country_name"`
	CountryDisplayName  string           `json:"country_display_name"`
	CountryFlag         string           `json:"country_flag"`
	VIP                 map[string]any   `json:"vip"`
	Level               map[string]any   `json:"level"`
	LevelBadges         map[string]any   `json:"level_badges"`
	Badges              []map[string]any `json:"badges"`
	AvatarFrame         map[string]any   `json:"avatar_frame"`
	ProfileCard         map[string]any   `json:"profile_card"`
	Vehicle             map[string]any   `json:"vehicle"`
	ChatBubble          map[string]any   `json:"chat_bubble"`
	MicSeatIcon         map[string]any   `json:"mic_seat_icon"`
	MicSeatAnimation    map[string]any   `json:"mic_seat_animation"`
	Charm               int64            `json:"charm"`
}

type roomBasicProfileData struct {
	UserID        string `json:"user_id"`
	Username      string `json:"username"`
	Avatar        string `json:"avatar"`
	DisplayUserID string `json:"display_user_id"`
}

type roomGiftPanelData struct {
	CoinBalance     int64                   `json:"coin_balance"`
	Recipients      []roomGiftRecipientData `json:"recipients"`
	Tabs            []roomGiftTabData       `json:"tabs"`
	Gifts           []giftConfigData        `json:"gifts"`
	QuantityPresets []int32                 `json:"quantity_presets"`
	// ComboConfig is additive: old Flutter models ignore it, while V2 clients receive batching,
	// concurrency, retry and rollout policy in the same response that opens the gift panel.
	ComboConfig appconfig.GiftComboConfig `json:"combo_config"`
}

type roomRocketData struct {
	Enabled           bool                  `json:"enabled"`
	Levels            []roomRocketLevelData `json:"levels"`
	State             roomRocketStateData   `json:"state"`
	BroadcastScope    string                `json:"broadcast_scope"`
	LaunchDelayMS     int64                 `json:"launch_delay_ms"`
	BroadcastDelayMS  int64                 `json:"broadcast_delay_ms"`
	RewardStackPolicy string                `json:"reward_stack_policy"`
	ServerTimeMS      int64                 `json:"server_time_ms"`
}

type roomRocketLevelData struct {
	Level              int32                  `json:"level"`
	FuelThreshold      int64                  `json:"fuel_threshold"`
	CoverURL           string                 `json:"cover_url,omitempty"`
	AnimationURL       string                 `json:"animation_url,omitempty"`
	LaunchAnimationURL string                 `json:"launch_animation_url,omitempty"`
	LaunchedImageURL   string                 `json:"launched_image_url,omitempty"`
	InRoomRewards      []roomRocketRewardData `json:"in_room_rewards"`
	Top1Rewards        []roomRocketRewardData `json:"top1_rewards"`
	IgniterRewards     []roomRocketRewardData `json:"igniter_rewards"`
}

type roomRocketRewardData struct {
	RewardItemID    string                       `json:"reward_item_id"`
	ResourceGroupID int64                        `json:"resource_group_id"`
	ResourceGroup   *roomRocketResourceGroupData `json:"resource_group,omitempty"`
	Weight          int64                        `json:"weight,omitempty"`
	DisplayName     string                       `json:"display_name,omitempty"`
	IconURL         string                       `json:"icon_url,omitempty"`
}

type roomRocketStateData struct {
	CurrentLevel    int32                         `json:"current_level"`
	CurrentFuel     int64                         `json:"current_fuel"`
	FuelThreshold   int64                         `json:"fuel_threshold"`
	Status          string                        `json:"status"`
	IgnitedAtMS     int64                         `json:"ignited_at_ms,omitempty"`
	LaunchAtMS      int64                         `json:"launch_at_ms,omitempty"`
	LaunchedAtMS    int64                         `json:"launched_at_ms,omitempty"`
	ResetAtMS       int64                         `json:"reset_at_ms"`
	Top1UserID      string                        `json:"top1_user_id,omitempty"`
	IgniterUserID   string                        `json:"igniter_user_id,omitempty"`
	RocketID        string                        `json:"rocket_id,omitempty"`
	ConfigVersion   int64                         `json:"config_version,omitempty"`
	LastRewards     []roomRocketRewardGrantData   `json:"last_rewards,omitempty"`
	PendingLaunches []roomRocketPendingLaunchData `json:"pending_launches,omitempty"`
}

type roomRocketPendingLaunchData struct {
	RocketID      string `json:"rocket_id"`
	Level         int32  `json:"level"`
	FuelThreshold int64  `json:"fuel_threshold"`
	IgnitedAtMS   int64  `json:"ignited_at_ms"`
	LaunchAtMS    int64  `json:"launch_at_ms"`
	ResetAtMS     int64  `json:"reset_at_ms"`
	Top1UserID    string `json:"top1_user_id,omitempty"`
	IgniterUserID string `json:"igniter_user_id,omitempty"`
	ConfigVersion int64  `json:"config_version,omitempty"`
	CoverURL      string `json:"cover_url,omitempty"`
}

type roomRocketRewardGrantData struct {
	RewardRole      string                       `json:"reward_role"`
	UserID          string                       `json:"user_id"`
	RewardItemID    string                       `json:"reward_item_id"`
	ResourceGroupID int64                        `json:"resource_group_id"`
	ResourceGroup   *roomRocketResourceGroupData `json:"resource_group,omitempty"`
	DisplayName     string                       `json:"display_name,omitempty"`
	IconURL         string                       `json:"icon_url,omitempty"`
	GrantID         string                       `json:"grant_id,omitempty"`
	Status          string                       `json:"status,omitempty"`
}

type roomRocketResourceData struct {
	ResourceID        int64    `json:"resource_id"`
	ResourceCode      string   `json:"resource_code"`
	ResourceType      string   `json:"resource_type"`
	Name              string   `json:"name"`
	Status            string   `json:"status"`
	Grantable         bool     `json:"grantable"`
	GrantStrategy     string   `json:"grant_strategy"`
	WalletAssetType   string   `json:"wallet_asset_type,omitempty"`
	WalletAssetAmount int64    `json:"wallet_asset_amount,omitempty"`
	UsageScopes       []string `json:"usage_scopes"`
	AssetURL          string   `json:"asset_url"`
	PreviewURL        string   `json:"preview_url"`
	AnimationURL      string   `json:"animation_url"`
	MetadataJSON      string   `json:"metadata_json"`
	SortOrder         int32    `json:"sort_order"`
	CreatedAtMS       int64    `json:"created_at_ms"`
	UpdatedAtMS       int64    `json:"updated_at_ms"`
}

type roomRocketResourceGroupItemData struct {
	GroupItemID       int64                   `json:"group_item_id"`
	ItemType          string                  `json:"item_type"`
	ResourceID        int64                   `json:"resource_id"`
	Resource          *roomRocketResourceData `json:"resource,omitempty"`
	WalletAssetType   string                  `json:"wallet_asset_type,omitempty"`
	WalletAssetAmount int64                   `json:"wallet_asset_amount,omitempty"`
	Quantity          int64                   `json:"quantity"`
	DurationMS        int64                   `json:"duration_ms"`
	SortOrder         int32                   `json:"sort_order"`
}

type roomRocketResourceGroupData struct {
	GroupID     int64                             `json:"group_id"`
	GroupCode   string                            `json:"group_code"`
	Name        string                            `json:"name"`
	Status      string                            `json:"status"`
	Description string                            `json:"description"`
	SortOrder   int32                             `json:"sort_order"`
	Items       []roomRocketResourceGroupItemData `json:"items"`
	CreatedAtMS int64                             `json:"created_at_ms"`
	UpdatedAtMS int64                             `json:"updated_at_ms"`
}

type roomGiftRecipientData struct {
	TargetType string                  `json:"target_type"`
	UserID     string                  `json:"user_id,omitempty"`
	SeatNo     int32                   `json:"seat_no,omitempty"`
	Label      string                  `json:"label,omitempty"`
	Profile    *roomDisplayProfileData `json:"profile,omitempty"`
}

type roomGiftTabData struct {
	Key          string `json:"key"`
	GiftTypeCode string `json:"gift_type_code,omitempty"`
	Label        string `json:"label,omitempty"`
	Order        int32  `json:"order"`
}

type updateRoomProfileData struct {
	Result       roomCommandResultData `json:"result"`
	Room         roomInitialData       `json:"room"`
	Seats        []roomSeatData        `json:"seats"`
	ServerTimeMS int64                 `json:"server_time_ms"`
}

type roomBackgroundData struct {
	BackgroundID    int64          `json:"background_id"`
	RoomID          string         `json:"room_id"`
	ImageURL        string         `json:"image_url"`
	CreatedByUserID string         `json:"created_by_user_id,omitempty"`
	CreatedAtMS     int64          `json:"created_at_ms"`
	UpdatedAtMS     int64          `json:"updated_at_ms"`
	CommandID       string         `json:"command_id,omitempty"`
	Media           *roomMediaData `json:"media,omitempty"`
}

type saveRoomBackgroundData struct {
	Background   roomBackgroundData `json:"background"`
	ServerTimeMS int64              `json:"server_time_ms"`
}

type listRoomBackgroundsData struct {
	Backgrounds           []roomBackgroundData `json:"backgrounds"`
	SelectedBackgroundURL string               `json:"selected_background_url,omitempty"`
	SelectedBackground    *roomBackgroundData  `json:"selected_background,omitempty"`
	ServerTimeMS          int64                `json:"server_time_ms"`
}

type setRoomBackgroundData struct {
	Result       roomCommandResultData `json:"result"`
	Room         roomInitialData       `json:"room"`
	Background   roomBackgroundData    `json:"background"`
	ServerTimeMS int64                 `json:"server_time_ms"`
}

type roomSnapshotData struct {
	Room             roomInitialData    `json:"room"`
	Seats            []roomSeatData     `json:"seats"`
	ContributionRank []roomRankItemData `json:"contribution_rank"`
	IsFollowed       bool               `json:"is_followed"`
	ServerTimeMS     int64              `json:"server_time_ms"`
}

type roomFollowData struct {
	RoomID       string `json:"room_id"`
	IsFollowed   bool   `json:"is_followed"`
	FollowedAtMS int64  `json:"followed_at_ms,omitempty"`
	ServerTimeMS int64  `json:"server_time_ms"`
}

type roomCommandResultData struct {
	Applied      bool  `json:"applied"`
	RoomVersion  int64 `json:"room_version"`
	ServerTimeMS int64 `json:"server_time_ms"`
}

type roomInitialData struct {
	RoomID            string              `json:"room_id"`
	IMGroupID         string              `json:"im_group_id"`
	RoomShortID       string              `json:"room_short_id,omitempty"`
	Title             string              `json:"title,omitempty"`
	CoverURL          string              `json:"cover_url,omitempty"`
	BackgroundURL     string              `json:"background_url,omitempty"`
	Description       string              `json:"description,omitempty"`
	OwnerUserID       string              `json:"owner_user_id,omitempty"`
	Mode              string              `json:"mode,omitempty"`
	Status            string              `json:"status,omitempty"`
	ChatEnabled       bool                `json:"chat_enabled"`
	Locked            bool                `json:"locked"`
	Heat              int64               `json:"heat"`
	OnlineCount       int32               `json:"online_count"`
	SeatCount         int32               `json:"seat_count"`
	OccupiedSeatCount int32               `json:"occupied_seat_count"`
	VisibleRegionID   int64               `json:"visible_region_id,omitempty"`
	Version           int64               `json:"version"`
	ActiveBackground  *roomBackgroundData `json:"active_background,omitempty"`
	RoomBorderURL     string              `json:"room_border_url"`
	RoomBorder        *roomDecorationData `json:"room_border,omitempty"`
	RoomNameStyle     *roomDecorationData `json:"room_name_style,omitempty"`
}

type roomViewerData struct {
	UserID       string `json:"user_id"`
	Role         string `json:"role,omitempty"`
	JoinedAtMS   int64  `json:"joined_at_ms,omitempty"`
	LastSeenAtMS int64  `json:"last_seen_at_ms,omitempty"`
}

type roomSeatData struct {
	SeatNo            int32  `json:"seat_no"`
	UserID            string `json:"user_id,omitempty"`
	Locked            bool   `json:"locked"`
	PublishState      string `json:"publish_state,omitempty"`
	MicSessionID      string `json:"mic_session_id,omitempty"`
	PublishDeadlineMS int64  `json:"publish_deadline_ms,omitempty"`
	MicMuted          bool   `json:"mic_muted"`
	SeatStatus        string `json:"seat_status,omitempty"`
	MicHeartbeatAtMS  int64  `json:"mic_heartbeat_at_ms,omitempty"`
	GiftValue         int64  `json:"gift_value"`
}

type roomRankItemData struct {
	UserID      string `json:"user_id"`
	Score       int64  `json:"score"`
	GiftValue   int64  `json:"gift_value"`
	UpdatedAtMS int64  `json:"updated_at_ms,omitempty"`
}

type roomIMData struct {
	GroupID       string `json:"group_id"`
	NeedJoinGroup bool   `json:"need_join_group"`
}

type roomRTCData struct {
	NeedToken bool                    `json:"need_token"`
	Available bool                    `json:"available"`
	Token     *tencentrtc.TokenResult `json:"token,omitempty"`
	Reason    string                  `json:"reason,omitempty"`
}

func roomListDataFromProto(resp *roomv1.ListRoomsResponse) roomListData {
	if resp == nil {
		return roomListData{}
	}
	items := make([]roomListItemData, 0, len(resp.GetRooms()))
	for _, room := range resp.GetRooms() {
		items = append(items, roomListItemDataFromProto(room))
	}
	return roomListData{Rooms: items, NextCursor: resp.GetNextCursor()}
}

func myRoomDataFromProto(resp *roomv1.GetMyRoomResponse) myRoomData {
	if resp == nil {
		return myRoomData{}
	}
	data := myRoomData{HasRoom: resp.GetHasRoom(), ServerTimeMS: resp.GetServerTimeMs()}
	if resp.GetHasRoom() && resp.GetRoom() != nil {
		room := roomListItemDataFromProto(resp.GetRoom())
		data.Room = &room
	}
	return data
}

func createRoomDataFromProto(resp *roomv1.CreateRoomResponse) createRoomData {
	if resp == nil {
		return createRoomData{}
	}
	snapshot := resp.GetRoom()
	roomID := snapshot.GetRoomId()
	return createRoomData{
		Result:       commandResultDataFromProto(resp.GetResult()),
		Room:         roomInitialRoomDataFromSnapshot(snapshot, roomID),
		Seats:        roomSeatDataFromSnapshot(snapshot),
		IM:           roomIMData{GroupID: roomIMGroupID(roomID), NeedJoinGroup: true},
		ServerTimeMS: resp.GetResult().GetServerTimeMs(),
	}
}

func roomRocketDataFromProto(resp *roomv1.GetRoomRocketResponse, resourceGroups map[int64]*roomRocketResourceGroupData) roomRocketData {
	if resp == nil || resp.GetRocket() == nil {
		return roomRocketData{}
	}
	rocket := resp.GetRocket()
	return roomRocketData{
		Enabled:           rocket.GetEnabled(),
		Levels:            roomRocketLevelsFromProto(rocket.GetLevels(), resourceGroups),
		State:             roomRocketStateFromProto(rocket.GetState(), resourceGroups),
		BroadcastScope:    rocket.GetBroadcastScope(),
		LaunchDelayMS:     rocket.GetLaunchDelayMs(),
		BroadcastDelayMS:  rocket.GetBroadcastDelayMs(),
		RewardStackPolicy: rocket.GetRewardStackPolicy(),
		ServerTimeMS:      resp.GetServerTimeMs(),
	}
}

func roomRocketLevelsFromProto(levels []*roomv1.RoomRocketLevel, resourceGroups map[int64]*roomRocketResourceGroupData) []roomRocketLevelData {
	out := make([]roomRocketLevelData, 0, len(levels))
	for _, level := range levels {
		out = append(out, roomRocketLevelData{
			Level:              level.GetLevel(),
			FuelThreshold:      level.GetFuelThreshold(),
			CoverURL:           level.GetCoverUrl(),
			AnimationURL:       level.GetAnimationUrl(),
			LaunchAnimationURL: level.GetLaunchAnimationUrl(),
			LaunchedImageURL:   level.GetLaunchedImageUrl(),
			InRoomRewards:      roomRocketRewardsFromProto(level.GetInRoomRewards(), resourceGroups),
			Top1Rewards:        roomRocketRewardsFromProto(level.GetTop1Rewards(), resourceGroups),
			IgniterRewards:     roomRocketRewardsFromProto(level.GetIgniterRewards(), resourceGroups),
		})
	}
	return out
}

func roomRocketRewardsFromProto(rewards []*roomv1.RoomRocketRewardItem, resourceGroups map[int64]*roomRocketResourceGroupData) []roomRocketRewardData {
	out := make([]roomRocketRewardData, 0, len(rewards))
	for _, reward := range rewards {
		out = append(out, roomRocketRewardData{
			RewardItemID:    reward.GetRewardItemId(),
			ResourceGroupID: reward.GetResourceGroupId(),
			ResourceGroup:   resourceGroups[reward.GetResourceGroupId()],
			Weight:          reward.GetWeight(),
			DisplayName:     reward.GetDisplayName(),
			IconURL:         reward.GetIconUrl(),
		})
	}
	return out
}

func roomRocketStateFromProto(state *roomv1.RoomRocketState, resourceGroups map[int64]*roomRocketResourceGroupData) roomRocketStateData {
	if state == nil {
		return roomRocketStateData{}
	}
	return roomRocketStateData{
		CurrentLevel:    state.GetCurrentLevel(),
		CurrentFuel:     state.GetCurrentFuel(),
		FuelThreshold:   state.GetFuelThreshold(),
		Status:          state.GetStatus(),
		IgnitedAtMS:     state.GetIgnitedAtMs(),
		LaunchAtMS:      state.GetLaunchAtMs(),
		LaunchedAtMS:    state.GetLaunchedAtMs(),
		ResetAtMS:       state.GetResetAtMs(),
		Top1UserID:      formatOptionalUserID(state.GetTop1UserId()),
		IgniterUserID:   formatOptionalUserID(state.GetIgniterUserId()),
		RocketID:        state.GetRocketId(),
		ConfigVersion:   state.GetConfigVersion(),
		LastRewards:     roomRocketRewardGrantsFromProto(state.GetLastRewards(), resourceGroups),
		PendingLaunches: roomRocketPendingLaunchesFromProto(state.GetPendingLaunches()),
	}
}

func roomRocketPendingLaunchesFromProto(launches []*roomv1.RoomRocketPendingLaunch) []roomRocketPendingLaunchData {
	out := make([]roomRocketPendingLaunchData, 0, len(launches))
	for _, launch := range launches {
		out = append(out, roomRocketPendingLaunchData{
			RocketID:      launch.GetRocketId(),
			Level:         launch.GetLevel(),
			FuelThreshold: launch.GetFuelThreshold(),
			IgnitedAtMS:   launch.GetIgnitedAtMs(),
			LaunchAtMS:    launch.GetLaunchAtMs(),
			ResetAtMS:     launch.GetResetAtMs(),
			Top1UserID:    formatOptionalUserID(launch.GetTop1UserId()),
			IgniterUserID: formatOptionalUserID(launch.GetIgniterUserId()),
			ConfigVersion: launch.GetConfigVersion(),
			CoverURL:      launch.GetCoverUrl(),
		})
	}
	return out
}

func roomRocketRewardGrantsFromProto(rewards []*roomv1.RoomRocketRewardGrant, resourceGroups map[int64]*roomRocketResourceGroupData) []roomRocketRewardGrantData {
	out := make([]roomRocketRewardGrantData, 0, len(rewards))
	for _, reward := range rewards {
		out = append(out, roomRocketRewardGrantData{
			RewardRole:      reward.GetRewardRole(),
			UserID:          formatOptionalUserID(reward.GetUserId()),
			RewardItemID:    reward.GetRewardItemId(),
			ResourceGroupID: reward.GetResourceGroupId(),
			ResourceGroup:   resourceGroups[reward.GetResourceGroupId()],
			DisplayName:     reward.GetDisplayName(),
			IconURL:         reward.GetIconUrl(),
			GrantID:         reward.GetGrantId(),
			Status:          reward.GetStatus(),
		})
	}
	return out
}

func roomRocketResourceGroupIDs(resp *roomv1.GetRoomRocketResponse) []int64 {
	if resp == nil || resp.GetRocket() == nil {
		return nil
	}
	seen := map[int64]struct{}{}
	ids := make([]int64, 0)
	add := func(id int64) {
		if id <= 0 {
			return
		}
		if _, exists := seen[id]; exists {
			return
		}
		seen[id] = struct{}{}
		ids = append(ids, id)
	}
	for _, level := range resp.GetRocket().GetLevels() {
		for _, reward := range level.GetInRoomRewards() {
			add(reward.GetResourceGroupId())
		}
		for _, reward := range level.GetTop1Rewards() {
			add(reward.GetResourceGroupId())
		}
		for _, reward := range level.GetIgniterRewards() {
			add(reward.GetResourceGroupId())
		}
	}
	for _, reward := range resp.GetRocket().GetState().GetLastRewards() {
		add(reward.GetResourceGroupId())
	}
	return ids
}

func roomRocketResourceGroupFromProto(group *walletv1.ResourceGroup) *roomRocketResourceGroupData {
	if group == nil {
		return nil
	}
	items := make([]roomRocketResourceGroupItemData, 0, len(group.GetItems()))
	for _, item := range group.GetItems() {
		items = append(items, roomRocketResourceGroupItemData{
			GroupItemID:       item.GetGroupItemId(),
			ItemType:          item.GetItemType(),
			ResourceID:        item.GetResourceId(),
			Resource:          roomRocketResourceFromProto(item.GetResource()),
			WalletAssetType:   item.GetWalletAssetType(),
			WalletAssetAmount: item.GetWalletAssetAmount(),
			Quantity:          item.GetQuantity(),
			DurationMS:        item.GetDurationMs(),
			SortOrder:         item.GetSortOrder(),
		})
	}
	return &roomRocketResourceGroupData{
		GroupID:     group.GetGroupId(),
		GroupCode:   group.GetGroupCode(),
		Name:        group.GetName(),
		Status:      group.GetStatus(),
		Description: group.GetDescription(),
		SortOrder:   group.GetSortOrder(),
		Items:       items,
		CreatedAtMS: group.GetCreatedAtMs(),
		UpdatedAtMS: group.GetUpdatedAtMs(),
	}
}

func roomRocketResourceFromProto(item *walletv1.Resource) *roomRocketResourceData {
	if item == nil {
		return nil
	}
	return &roomRocketResourceData{
		ResourceID:        item.GetResourceId(),
		ResourceCode:      item.GetResourceCode(),
		ResourceType:      item.GetResourceType(),
		Name:              item.GetName(),
		Status:            item.GetStatus(),
		Grantable:         item.GetGrantable(),
		GrantStrategy:     item.GetGrantStrategy(),
		WalletAssetType:   item.GetWalletAssetType(),
		WalletAssetAmount: item.GetWalletAssetAmount(),
		UsageScopes:       item.GetUsageScopes(),
		AssetURL:          item.GetAssetUrl(),
		PreviewURL:        item.GetPreviewUrl(),
		AnimationURL:      item.GetAnimationUrl(),
		MetadataJSON:      item.GetMetadataJson(),
		SortOrder:         item.GetSortOrder(),
		CreatedAtMS:       item.GetCreatedAtMs(),
		UpdatedAtMS:       item.GetUpdatedAtMs(),
	}
}

func roomListItemDataFromProto(room *roomv1.RoomListItem) roomListItemData {
	if room == nil {
		return roomListItemData{}
	}
	roomID := room.GetRoomId()
	return roomListItemData{
		RoomID:                     roomID,
		IMGroupID:                  roomIMGroupID(roomID),
		OwnerUserID:                formatOptionalUserID(room.GetOwnerUserId()),
		OwnerVIPLevel:              room.GetOwnerVipLevel(),
		Title:                      room.GetTitle(),
		CoverURL:                   room.GetCoverUrl(),
		Mode:                       room.GetMode(),
		Status:                     room.GetStatus(),
		Locked:                     room.GetLocked(),
		Heat:                       room.GetHeat(),
		OnlineCount:                room.GetOnlineCount(),
		SeatCount:                  room.GetSeatCount(),
		OccupiedSeatCount:          room.GetOccupiedSeatCount(),
		VisibleRegionID:            room.GetVisibleRegionId(),
		AppCode:                    room.GetAppCode(),
		RoomShortID:                room.GetRoomShortId(),
		CountryCode:                room.GetCountryCode(),
		CountryFlag:                roomProfileCountryFlag(room.GetCountryCode()),
		RoomBorderURL:              roomBorderURL(room.GetRoomBorderUrl(), room.GetRoomBorder()),
		RoomBorder:                 roomDecorationDataFromProto(room.GetRoomBorder()),
		RoomNameStyle:              roomDecorationDataFromProto(room.GetRoomNameStyle()),
		RocketNearCompletion:       room.GetRocketNearCompletion(),
		HasActiveRedPacket:         room.GetHasActiveRedPacket(),
		ActiveRedPacketExpiresAtMS: room.GetActiveRedPacketExpiresAtMs(),
	}
}

func commandResultDataFromProto(result *roomv1.CommandResult) roomCommandResultData {
	if result == nil {
		return roomCommandResultData{}
	}
	return roomCommandResultData{
		Applied:      result.GetApplied(),
		RoomVersion:  result.GetRoomVersion(),
		ServerTimeMS: result.GetServerTimeMs(),
	}
}

func updateRoomProfileDataFromProto(resp *roomv1.UpdateRoomProfileResponse) updateRoomProfileData {
	if resp == nil {
		return updateRoomProfileData{}
	}
	snapshot := resp.GetRoom()
	return updateRoomProfileData{
		Result:       commandResultDataFromProto(resp.GetResult()),
		Room:         roomInitialRoomDataFromSnapshot(snapshot, snapshot.GetRoomId()),
		Seats:        roomSeatDataFromSnapshot(snapshot),
		ServerTimeMS: resp.GetResult().GetServerTimeMs(),
	}
}

func saveRoomBackgroundDataFromProto(resp *roomv1.SaveRoomBackgroundResponse) saveRoomBackgroundData {
	if resp == nil {
		return saveRoomBackgroundData{}
	}
	return saveRoomBackgroundData{
		Background:   roomBackgroundDataFromProto(resp.GetBackground()),
		ServerTimeMS: resp.GetServerTimeMs(),
	}
}

func listRoomBackgroundsDataFromProto(resp *roomv1.ListRoomBackgroundsResponse) listRoomBackgroundsData {
	if resp == nil {
		return listRoomBackgroundsData{}
	}
	backgrounds := make([]roomBackgroundData, 0, len(resp.GetBackgrounds()))
	for _, background := range resp.GetBackgrounds() {
		backgrounds = append(backgrounds, roomBackgroundDataFromProto(background))
	}
	return listRoomBackgroundsData{
		Backgrounds:           backgrounds,
		SelectedBackgroundURL: resp.GetSelectedBackgroundUrl(),
		SelectedBackground:    roomBackgroundDataPointerFromProto(resp.GetSelectedBackground()),
		ServerTimeMS:          resp.GetServerTimeMs(),
	}
}

func setRoomBackgroundDataFromProto(resp *roomv1.SetRoomBackgroundResponse) setRoomBackgroundData {
	if resp == nil {
		return setRoomBackgroundData{}
	}
	snapshot := resp.GetRoom()
	return setRoomBackgroundData{
		Result:       commandResultDataFromProto(resp.GetResult()),
		Room:         roomInitialRoomDataFromSnapshot(snapshot, snapshot.GetRoomId()),
		Background:   roomBackgroundDataFromProto(resp.GetBackground()),
		ServerTimeMS: resp.GetResult().GetServerTimeMs(),
	}
}

func roomBackgroundDataFromProto(background *roomv1.RoomBackgroundImage) roomBackgroundData {
	if background == nil {
		return roomBackgroundData{}
	}
	return roomBackgroundData{
		BackgroundID:    background.GetBackgroundId(),
		RoomID:          background.GetRoomId(),
		ImageURL:        background.GetImageUrl(),
		CreatedByUserID: formatOptionalUserID(background.GetCreatedByUserId()),
		CreatedAtMS:     background.GetCreatedAtMs(),
		UpdatedAtMS:     background.GetUpdatedAtMs(),
		CommandID:       background.GetCommandId(),
		Media:           roomMediaDataFromProto(background.GetMedia()),
	}
}

func roomBackgroundDataPointerFromProto(background *roomv1.RoomBackgroundImage) *roomBackgroundData {
	if background == nil || background.GetBackgroundId() <= 0 {
		return nil
	}
	data := roomBackgroundDataFromProto(background)
	return &data
}

func roomSnapshotDataFromProto(resp *roomv1.GetRoomSnapshotResponse) roomSnapshotData {
	if resp == nil {
		return roomSnapshotData{}
	}
	snapshot := resp.GetRoom()
	return roomSnapshotData{
		Room:             roomInitialRoomDataFromSnapshot(snapshot, snapshot.GetRoomId()),
		Seats:            roomSeatDataFromSnapshot(snapshot),
		ContributionRank: roomRankDataFromSnapshot(snapshot, 0),
		IsFollowed:       resp.GetIsFollowed(),
		ServerTimeMS:     resp.GetServerTimeMs(),
	}
}

func roomInitialRoomDataFromSnapshot(snapshot *roomv1.RoomSnapshot, roomID string) roomInitialData {
	if snapshot == nil {
		return roomInitialData{RoomID: roomID, IMGroupID: roomIMGroupID(roomID)}
	}
	ext := snapshot.GetRoomExt()
	if roomID == "" {
		roomID = snapshot.GetRoomId()
	}
	roomShortID := snapshot.GetRoomShortId()
	if roomShortID == "" {
		roomShortID = ext["room_short_id"]
	}
	seatCount, occupiedSeatCount := roomSeatCounts(snapshot)
	onlineCount := snapshot.GetOnlineCount()
	if onlineCount <= 0 && len(snapshot.GetOnlineUsers()) > 0 {
		// 老快照或非 lite 上游可能还没有 online_count 字段；此时用明细长度保持兼容。
		onlineCount = int32(len(snapshot.GetOnlineUsers()))
	}
	return roomInitialData{
		RoomID:            roomID,
		IMGroupID:         roomIMGroupID(roomID),
		RoomShortID:       roomShortID,
		Title:             ext["title"],
		CoverURL:          ext["cover_url"],
		BackgroundURL:     ext["background_url"],
		Description:       ext["description"],
		OwnerUserID:       formatOptionalUserID(snapshot.GetOwnerUserId()),
		Mode:              snapshot.GetMode(),
		Status:            snapshot.GetStatus(),
		ChatEnabled:       snapshot.GetChatEnabled(),
		Locked:            snapshot.GetLocked(),
		Heat:              snapshot.GetHeat(),
		OnlineCount:       onlineCount,
		SeatCount:         seatCount,
		OccupiedSeatCount: occupiedSeatCount,
		VisibleRegionID:   snapshot.GetVisibleRegionId(),
		Version:           snapshot.GetVersion(),
		ActiveBackground:  roomBackgroundDataPointerFromProto(snapshot.GetActiveBackground()),
		RoomBorderURL:     roomBorderURL(snapshot.GetRoomBorderUrl(), snapshot.GetRoomBorder()),
		RoomBorder:        roomDecorationDataFromProto(snapshot.GetRoomBorder()),
		RoomNameStyle:     roomDecorationDataFromProto(snapshot.GetRoomNameStyle()),
	}
}

func roomBorderURL(flatURL string, resource *roomv1.RoomDecorationResource) string {
	if flatURL != "" {
		return flatURL
	}
	// 滚动发布期间旧 room-service 只有结构化 room_border；gateway 从同一资源快照派生 URL，不额外查询。
	return resource.GetAssetUrl()
}

func roomViewerDataFromProto(user *roomv1.RoomUser, fallbackUserID int64) roomViewerData {
	userID := fallbackUserID
	if user.GetUserId() > 0 {
		userID = user.GetUserId()
	}
	return roomViewerData{
		UserID:       formatOptionalUserID(userID),
		Role:         user.GetRole(),
		JoinedAtMS:   user.GetJoinedAtMs(),
		LastSeenAtMS: user.GetLastSeenAtMs(),
	}
}

func roomSeatDataFromSnapshot(snapshot *roomv1.RoomSnapshot) []roomSeatData {
	seats := snapshot.GetMicSeats()
	items := make([]roomSeatData, 0, len(seats))
	for _, seat := range seats {
		items = append(items, roomSeatData{
			SeatNo:            seat.GetSeatNo(),
			UserID:            formatOptionalUserID(seat.GetUserId()),
			Locked:            seat.GetLocked(),
			PublishState:      seat.GetPublishState(),
			MicSessionID:      seat.GetMicSessionId(),
			PublishDeadlineMS: seat.GetPublishDeadlineMs(),
			MicMuted:          seat.GetMicMuted(),
			SeatStatus:        seat.GetSeatStatus(),
			MicHeartbeatAtMS:  seat.GetMicHeartbeatAtMs(),
			GiftValue:         seat.GetGiftValue(),
		})
	}
	return items
}

func roomDetailDataFromSnapshot(resp *roomv1.GetRoomSnapshotResponse, viewerUserID int64, rtc roomRTCData, profiles []roomDisplayProfileData) roomDetailData {
	if resp == nil {
		return roomDetailData{}
	}
	snapshot := resp.GetRoom()
	roomID := snapshot.GetRoomId()
	viewer := protoRoomUserByID(snapshot, viewerUserID)
	return roomDetailData{
		Room:         roomInitialRoomDataFromSnapshot(snapshot, roomID),
		Viewer:       roomViewerDataFromProto(viewer, viewerUserID),
		IsFollowed:   resp.GetIsFollowed(),
		Seats:        roomSeatDataFromSnapshot(snapshot),
		RankTop:      roomRankDataFromSnapshot(snapshot, joinRoomContributionRankLimit),
		OnlineCount:  int32(len(snapshot.GetOnlineUsers())),
		Permissions:  roomPermissionsFromSnapshot(snapshot, viewerUserID),
		Profiles:     profiles,
		IM:           roomIMData{GroupID: roomIMGroupID(roomID), NeedJoinGroup: true},
		RTC:          rtc,
		ServerTimeMS: resp.GetServerTimeMs(),
	}
}

func roomFollowDataFromProto(resp *roomv1.FollowRoomResponse) roomFollowData {
	if resp == nil {
		return roomFollowData{}
	}
	return roomFollowData{
		RoomID:       resp.GetRoomId(),
		IsFollowed:   resp.GetFollowed(),
		FollowedAtMS: resp.GetFollowedAtMs(),
		ServerTimeMS: resp.GetServerTimeMs(),
	}
}

func roomUnfollowDataFromProto(resp *roomv1.UnfollowRoomResponse) roomFollowData {
	if resp == nil {
		return roomFollowData{}
	}
	return roomFollowData{
		RoomID:       resp.GetRoomId(),
		IsFollowed:   resp.GetFollowed(),
		ServerTimeMS: resp.GetServerTimeMs(),
	}
}

func roomPermissionsFromSnapshot(snapshot *roomv1.RoomSnapshot, viewerUserID int64) roomPermissionsData {
	if snapshot == nil || viewerUserID <= 0 {
		return roomPermissionsData{}
	}
	isOwner := snapshot.GetOwnerUserId() == viewerUserID
	isAdmin := !isOwner && containsInt64(snapshot.GetAdminUserIds(), viewerUserID)
	isMuted := containsInt64(snapshot.GetMuteUserIds(), viewerUserID)
	inRoom := protoRoomUserByID(snapshot, viewerUserID) != nil
	return roomPermissionsData{
		IsOwner:     isOwner,
		IsAdmin:     isAdmin,
		IsMuted:     isMuted,
		CanSpeak:    inRoom && snapshot.GetChatEnabled() && !isMuted,
		CanSendGift: inRoom && snapshot.GetStatus() == "active",
		// can_mic_up 只表达服务端当前可执行性；禁言用户即使在线且麦位空闲也不能引导进入必然失败的 MicUp。
		CanMicUp:       inRoom && snapshot.GetStatus() == "active" && !isMuted && protoSeatByUserID(snapshot, viewerUserID) == nil,
		CanManageRoom:  inRoom && (isOwner || isAdmin),
		CanLeaveRoom:   inRoom,
		CanCloseRoom:   inRoom,
		CanMuteMic:     inRoom && protoSeatByUserID(snapshot, viewerUserID) != nil,
		CanOpenGiftBox: inRoom && snapshot.GetStatus() == "active",
	}
}

func roomRankDataFromSnapshot(snapshot *roomv1.RoomSnapshot, limit int) []roomRankItemData {
	rank := snapshot.GetGiftRank()
	if limit > 0 && len(rank) > limit {
		rank = rank[:limit]
	}
	items := make([]roomRankItemData, 0, len(rank))
	for _, item := range rank {
		items = append(items, roomRankItemData{
			UserID:      formatOptionalUserID(item.GetUserId()),
			Score:       item.GetScore(),
			GiftValue:   item.GetGiftValue(),
			UpdatedAtMS: item.GetUpdatedAtMs(),
		})
	}
	return items
}

func roomInitialProfileUserIDs(snapshot *roomv1.RoomSnapshot, viewerUserID int64, rankLimit int) []int64 {
	seen := map[int64]bool{}
	userIDs := make([]int64, 0, 16)
	add := func(userID int64) {
		if userID <= 0 || seen[userID] {
			return
		}
		seen[userID] = true
		userIDs = append(userIDs, userID)
	}

	add(snapshot.GetOwnerUserId())
	add(viewerUserID)
	for _, seat := range snapshot.GetMicSeats() {
		add(seat.GetUserId())
	}
	rank := snapshot.GetGiftRank()
	if rankLimit > 0 && len(rank) > rankLimit {
		rank = rank[:rankLimit]
	}
	for _, item := range rank {
		add(item.GetUserId())
	}
	for _, userID := range robotRoomProfileUserIDs(snapshot) {
		// 机器人房间的礼物时间线会马上用机器人 sender/target 资料；首屏带出这些 profile，避免 Flutter 落到 User <id> 兜底。
		add(userID)
	}
	return userIDs
}

func roomInitialBasicProfileUserIDs(snapshot *roomv1.RoomSnapshot, viewerUserID int64) []int64 {
	seen := map[int64]bool{}
	userIDs := make([]int64, 0, len(snapshot.GetMicSeats())+2)
	add := func(userID int64) {
		if userID <= 0 || seen[userID] {
			return
		}
		seen[userID] = true
		userIDs = append(userIDs, userID)
	}

	add(snapshot.GetOwnerUserId())
	add(viewerUserID)
	for _, seat := range snapshot.GetMicSeats() {
		add(seat.GetUserId())
	}
	return userIDs
}

func roomOnlineUserDataFromProto(resp *roomv1.ListRoomOnlineUsersResponse, profiles map[int64]roomDisplayProfileData) roomOnlineUsersData {
	if resp == nil {
		return roomOnlineUsersData{}
	}
	source := resp.GetItems()
	if len(source) == 0 {
		source = roomOnlineUsersFromLegacyUsers(resp.GetUsers())
	}
	items := make([]roomOnlineUserData, 0, len(source))
	for _, user := range source {
		item := roomOnlineUserData{
			UserID:       formatOptionalUserID(user.GetUserId()),
			Role:         user.GetRole(),
			IsOwner:      user.GetIsOwner(),
			RoomRole:     user.GetRoomRole(),
			GiftValue:    user.GetGiftValue(),
			JoinedAtMS:   user.GetJoinedAtMs(),
			LastSeenAtMS: user.GetLastSeenAtMs(),
		}
		if profile, ok := profiles[user.GetUserId()]; ok {
			copied := profile
			item.Profile = &copied
		}
		items = append(items, item)
	}
	return roomOnlineUsersData{
		Items:        items,
		Total:        resp.GetTotal(),
		Page:         resp.GetPage(),
		PageSize:     resp.GetPageSize(),
		ServerTimeMS: resp.GetServerTimeMs(),
	}
}

func roomBannedUsersDataFromProto(roomID string, resp *roomv1.ListRoomBannedUsersResponse, profiles map[int64]roomDisplayProfileData) roomBannedUsersData {
	if resp == nil {
		return roomBannedUsersData{RoomID: roomID}
	}
	items := make([]roomBannedUserData, 0, len(resp.GetItems()))
	for _, user := range resp.GetItems() {
		item := roomBannedUserData{
			UserID:      formatOptionalUserID(user.GetUserId()),
			ActorUserID: formatOptionalUserID(user.GetActorUserId()),
			CreatedAtMS: user.GetCreatedAtMs(),
			UnbanAtMS:   user.GetUnbanAtMs(),
			RemainingMS: user.GetRemainingMs(),
			Permanent:   user.GetPermanent(),
		}
		if profile, ok := profiles[user.GetUserId()]; ok {
			item.DisplayUserID = profile.DisplayUserID
			item.Username = profile.Username
			item.Avatar = profile.Avatar
			item.Country = profile.Country
			item.CountryName = profile.CountryName
			item.CountryDisplayName = profile.CountryDisplayName
			item.CountryFlag = profile.CountryFlag
		}
		items = append(items, item)
	}
	return roomBannedUsersData{
		RoomID:       roomID,
		Items:        items,
		Total:        resp.GetTotal(),
		Page:         resp.GetPage(),
		PageSize:     resp.GetPageSize(),
		ServerTimeMS: resp.GetServerTimeMs(),
	}
}

func roomContributionRankDataFromProto(resp *roomv1.ListRoomContributionRankResponse, profiles map[int64]roomDisplayProfileData) roomContributionRankData {
	if resp == nil {
		return roomContributionRankData{}
	}
	items := make([]roomContributionRankItemData, 0, len(resp.GetItems()))
	for _, rankItem := range resp.GetItems() {
		item := roomContributionRankItemData{
			Rank:        rankItem.GetRank(),
			UserID:      formatOptionalUserID(rankItem.GetUserId()),
			Score:       rankItem.GetScore(),
			GiftValue:   rankItem.GetGiftValue(),
			UpdatedAtMS: rankItem.GetUpdatedAtMs(),
		}
		if profile, ok := profiles[rankItem.GetUserId()]; ok {
			copied := profile
			item.Profile = &copied
		}
		items = append(items, item)
	}
	profileItems := make([]roomDisplayProfileData, 0, len(profiles))
	for _, rankItem := range resp.GetItems() {
		if profile, ok := profiles[rankItem.GetUserId()]; ok {
			profileItems = append(profileItems, profile)
		}
	}
	return roomContributionRankData{
		RoomID:           resp.GetRoomId(),
		Period:           resp.GetPeriod(),
		StartAtMS:        resp.GetStartAtMs(),
		EndAtMS:          resp.GetEndAtMs(),
		Total:            resp.GetTotal(),
		ContributionRank: items,
		Profiles:         profileItems,
		ServerTimeMS:     resp.GetServerTimeMs(),
	}
}

func roomOnlineUsersFromLegacyUsers(users []*roomv1.RoomUser) []*roomv1.RoomOnlineUser {
	items := make([]*roomv1.RoomOnlineUser, 0, len(users))
	for _, user := range users {
		items = append(items, &roomv1.RoomOnlineUser{
			UserId:       user.GetUserId(),
			Role:         user.GetRole(),
			JoinedAtMs:   user.GetJoinedAtMs(),
			LastSeenAtMs: user.GetLastSeenAtMs(),
			RoomRole:     "normal",
		})
	}
	return items
}

func protoRoomUserByID(snapshot *roomv1.RoomSnapshot, userID int64) *roomv1.RoomUser {
	if snapshot == nil || userID <= 0 {
		return nil
	}
	for _, user := range snapshot.GetOnlineUsers() {
		if user.GetUserId() == userID {
			return user
		}
	}
	return nil
}

func protoSeatByUserID(snapshot *roomv1.RoomSnapshot, userID int64) *roomv1.SeatState {
	if snapshot == nil || userID <= 0 {
		return nil
	}
	for _, seat := range snapshot.GetMicSeats() {
		if seat.GetUserId() == userID {
			return seat
		}
	}
	return nil
}

func containsInt64(values []int64, target int64) bool {
	for _, value := range values {
		if value == target {
			return true
		}
	}
	return false
}

func roomSeatCounts(snapshot *roomv1.RoomSnapshot) (int32, int32) {
	seatCount := int32(len(snapshot.GetMicSeats()))
	occupiedSeatCount := int32(0)
	for _, seat := range snapshot.GetMicSeats() {
		if seat.GetUserId() > 0 {
			occupiedSeatCount++
		}
	}
	return seatCount, occupiedSeatCount
}

func roomIMGroupID(roomID string) string {
	// 房间 IM 群 ID 必须等于内部 room_id；线上腾讯云已按这个 ID 建群，客户端也按这个字段入群。
	groupID, err := imgroup.RoomGroupID(roomID)
	if err != nil {
		return roomID
	}
	return groupID
}

func formatOptionalUserID(userID int64) string {
	if userID <= 0 {
		return ""
	}
	return strconv.FormatInt(userID, 10)
}
