package http

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

	roomv1 "hyapp.local/api/proto/room/v1"
	userv1 "hyapp.local/api/proto/user/v1"
	walletv1 "hyapp.local/api/proto/wallet/v1"
	"hyapp/pkg/appcode"
	"hyapp/pkg/roomid"
	"hyapp/pkg/xerr"
	"hyapp/services/gateway-service/internal/auth"
)

const (
	defaultRoomFeedRelationPageSize = 100
	maxRoomFeedRelationScanCount    = 500
)

type roomFeedRelationCursor struct {
	Tab           string `json:"tab"`
	Query         string `json:"query,omitempty"`
	UpdatedAtMS   int64  `json:"updated_at_ms,omitempty"`
	SubjectUserID int64  `json:"subject_user_id,omitempty"`
	RoomID        string `json:"room_id"`
}

// listRooms 按当前登录用户的服务端 region_id 查询房间发现列表。
func (h *Handler) listRooms(writer http.ResponseWriter, request *http.Request) {
	if h.userProfileClient == nil || h.roomQueryClient == nil {
		// 列表入口必须同时依赖 user-service 区域归属和 room-service 读模型。
		httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
		return
	}

	limit, ok := parseRoomListLimit(request.URL.Query().Get("limit"))
	if !ok {
		httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
		return
	}
	tab, ok := parsePublicRoomListTab(request.URL.Query().Get("tab"))
	if !ok {
		httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
		return
	}

	viewerUserID := auth.UserIDFromContext(request.Context())
	userResp, err := h.userProfileClient.GetUser(request.Context(), &userv1.GetUserRequest{
		Meta:   authRequestMeta(request, ""),
		UserId: viewerUserID,
	})
	if err != nil {
		// user-service 是 region_id 的唯一 owner，查询失败不能用 IP 国家兜底。
		httpkit.WriteRPCError(writer, request, err)
		return
	}

	resp, err := h.roomQueryClient.ListRooms(request.Context(), &roomv1.ListRoomsRequest{
		Meta:            meta(request, "", ""),
		ViewerUserId:    viewerUserID,
		VisibleRegionId: userResp.GetUser().GetRegionId(),
		Tab:             tab,
		Cursor:          request.URL.Query().Get("cursor"),
		Limit:           limit,
		Query:           roomListQuery(request),
	})
	if err != nil {
		httpkit.WriteRPCError(writer, request, err)
		return
	}
	httpkit.WriteOK(writer, request, roomListDataFromProto(resp))
}

// listRoomFeeds 查询 Mine 页 visited/friend/following 用户房间流。
func (h *Handler) listRoomFeeds(writer http.ResponseWriter, request *http.Request) {
	if h.userProfileClient == nil || h.roomQueryClient == nil {
		// feed 同样需要 user-service 区域归属，避免跨区域露出用户关系流房间。
		httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
		return
	}

	limit, ok := parseRoomListLimit(request.URL.Query().Get("limit"))
	if !ok {
		httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
		return
	}
	tab, ok := parseRoomFeedTab(request.URL.Query().Get("tab"))
	if !ok {
		httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
		return
	}

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

	relatedUsers, ok, err := h.roomFeedRelatedUsers(request, tab, roomListQuery(request), request.URL.Query().Get("cursor"))
	if !ok {
		httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
		return
	}
	if err != nil {
		httpkit.WriteRPCError(writer, request, err)
		return
	}

	resp, err := h.roomQueryClient.ListRoomFeeds(request.Context(), &roomv1.ListRoomFeedsRequest{
		Meta:            meta(request, "", ""),
		ViewerUserId:    viewerUserID,
		VisibleRegionId: userResp.GetUser().GetRegionId(),
		Tab:             tab,
		Cursor:          request.URL.Query().Get("cursor"),
		Limit:           limit,
		Query:           roomListQuery(request),
		RelatedUsers:    relatedUsers,
	})
	if err != nil {
		httpkit.WriteRPCError(writer, request, err)
		return
	}
	httpkit.WriteOK(writer, request, roomListDataFromProto(resp))
}

func (h *Handler) roomFeedRelatedUsers(request *http.Request, tab string, query string, rawCursor string) ([]*roomv1.RoomFeedRelatedUser, bool, error) {
	if tab == "visited" {
		return nil, true, nil
	}
	if h.userSocialClient == nil {
		// friend/following 必须以 user-service 关系事实为准，不能退回旧 room feed 投影。
		return nil, true, xerr.New(xerr.Unavailable, "user social client is not configured")
	}

	cursor, ok := decodeRoomFeedRelationCursor(tab, query, rawCursor)
	if !ok {
		return nil, false, nil
	}
	viewerUserID := auth.UserIDFromContext(request.Context())
	relatedUsers := make([]*roomv1.RoomFeedRelatedUser, 0, defaultRoomFeedRelationPageSize)
	for len(relatedUsers) < maxRoomFeedRelationScanCount {
		batch, nextCursor, err := h.roomFeedRelatedUserBatch(request, tab, viewerUserID, cursor)
		if err != nil {
			return nil, true, err
		}
		relatedUsers = append(relatedUsers, batch...)
		if len(batch) < defaultRoomFeedRelationPageSize {
			break
		}
		cursor = nextCursor
	}
	return relatedUsers, true, nil
}

func (h *Handler) roomFeedRelatedUserBatch(request *http.Request, tab string, viewerUserID int64, cursor roomFeedRelationCursor) ([]*roomv1.RoomFeedRelatedUser, roomFeedRelationCursor, error) {
	switch tab {
	case "following":
		resp, err := h.userSocialClient.ListFollowing(request.Context(), &userv1.ListFollowingRequest{
			Meta:              authRequestMeta(request, ""),
			UserId:            viewerUserID,
			Page:              1,
			PageSize:          defaultRoomFeedRelationPageSize,
			CursorUpdatedAtMs: cursor.UpdatedAtMS,
			CursorUserId:      cursor.SubjectUserID,
		})
		if err != nil {
			return nil, cursor, err
		}
		relatedUsers := make([]*roomv1.RoomFeedRelatedUser, 0, len(resp.GetRecords()))
		for _, record := range resp.GetRecords() {
			relatedUsers = append(relatedUsers, &roomv1.RoomFeedRelatedUser{
				UserId:              record.GetFolloweeUserId(),
				RelationUpdatedAtMs: record.GetFollowedAtMs(),
			})
			cursor.UpdatedAtMS = record.GetFollowedAtMs()
			cursor.SubjectUserID = record.GetFolloweeUserId()
		}
		return relatedUsers, cursor, nil
	case "friend":
		resp, err := h.userSocialClient.ListFriends(request.Context(), &userv1.ListFriendsRequest{
			Meta:              authRequestMeta(request, ""),
			UserId:            viewerUserID,
			Page:              1,
			PageSize:          defaultRoomFeedRelationPageSize,
			CursorUpdatedAtMs: cursor.UpdatedAtMS,
			CursorUserId:      cursor.SubjectUserID,
		})
		if err != nil {
			return nil, cursor, err
		}
		relatedUsers := make([]*roomv1.RoomFeedRelatedUser, 0, len(resp.GetRecords()))
		for _, record := range resp.GetRecords() {
			relatedUsers = append(relatedUsers, &roomv1.RoomFeedRelatedUser{
				UserId:              record.GetFriendUserId(),
				RelationUpdatedAtMs: record.GetFriendedAtMs(),
			})
			cursor.UpdatedAtMS = record.GetFriendedAtMs()
			cursor.SubjectUserID = record.GetFriendUserId()
		}
		return relatedUsers, cursor, nil
	default:
		return nil, cursor, xerr.New(xerr.InvalidArgument, "tab is invalid")
	}
}

func decodeRoomFeedRelationCursor(tab string, query string, raw string) (roomFeedRelationCursor, bool) {
	raw = strings.TrimSpace(raw)
	if raw == "" {
		return roomFeedRelationCursor{Tab: tab, Query: query}, true
	}
	payload, err := base64.RawURLEncoding.DecodeString(raw)
	if err != nil {
		return roomFeedRelationCursor{}, false
	}
	var cursor roomFeedRelationCursor
	if err := json.Unmarshal(payload, &cursor); err != nil {
		return roomFeedRelationCursor{}, false
	}
	if cursor.Tab != tab || cursor.Query != query || cursor.RoomID == "" || cursor.UpdatedAtMS <= 0 || cursor.SubjectUserID <= 0 {
		return roomFeedRelationCursor{}, false
	}
	return cursor, true
}

func parsePublicRoomListTab(raw string) (string, bool) {
	switch strings.ToLower(strings.TrimSpace(raw)) {
	case "", "hot":
		return "hot", true
	case "new":
		return "new", true
	default:
		return "", false
	}
}

func parseRoomFeedTab(raw string) (string, bool) {
	switch strings.ToLower(strings.TrimSpace(raw)) {
	case "visited":
		return "visited", true
	case "friend":
		return "friend", true
	case "following":
		return "following", true
	default:
		return "", false
	}
}

func roomListQuery(request *http.Request) string {
	if query := strings.TrimSpace(request.URL.Query().Get("q")); query != "" {
		return query
	}

	return strings.TrimSpace(request.URL.Query().Get("query"))
}

// parseRoomListLimit 只做 HTTP 字符串到数字的传输层解析。
// 默认值和最大值仍由 room-service 统一裁剪，避免 gateway 和查询服务出现双份策略。
func parseRoomListLimit(raw string) (int32, bool) {
	raw = strings.TrimSpace(raw)
	if raw == "" {
		return 0, true
	}

	value, err := strconv.Atoi(raw)
	if err != nil {
		return 0, false
	}

	return int32(value), true
}

// getMyRoom 返回 Mine 顶部“我的房间”卡片。
// 这个入口直接查 room-service 的 owner 权威数据，不依赖发现页 room_list_entries 投影是否命中。
func (h *Handler) getMyRoom(writer http.ResponseWriter, request *http.Request) {
	if h.roomQueryClient == nil {
		httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
		return
	}

	resp, err := h.roomQueryClient.GetMyRoom(request.Context(), &roomv1.GetMyRoomRequest{
		Meta:        meta(request, "", ""),
		OwnerUserId: auth.UserIDFromContext(request.Context()),
	})
	if err != nil {
		httpkit.WriteRPCError(writer, request, err)
		return
	}
	httpkit.WriteOK(writer, request, myRoomDataFromProto(resp))
}

// getCurrentRoom 查询当前登录用户是否仍有可恢复房间。
// gateway 只传鉴权 user_id；presence 和麦位状态由 room-service 用读模型和 Room Cell 快照判断。
func (h *Handler) getCurrentRoom(writer http.ResponseWriter, request *http.Request) {
	if h.roomQueryClient == nil {
		httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
		return
	}

	resp, err := h.roomQueryClient.GetCurrentRoom(request.Context(), &roomv1.GetCurrentRoomRequest{
		Meta:   meta(request, "", ""),
		UserId: auth.UserIDFromContext(request.Context()),
	})
	if err != nil {
		httpkit.WriteRPCError(writer, request, err)
		return
	}
	httpkit.WriteOK(writer, request, currentRoomDataFromProto(resp))
}

// currentRoomData 是 gateway 的外部 JSON 契约，显式保留 false/0 字段。
// 不能直接把 protobuf response 写出去，否则 proto3 的 omitempty 会让 has_current_room=false 消失。
type currentRoomData struct {
	HasCurrentRoom  bool   `json:"has_current_room"`
	RoomID          string `json:"room_id"`
	RoomVersion     int64  `json:"room_version"`
	Role            string `json:"role"`
	MicSessionID    string `json:"mic_session_id"`
	PublishState    string `json:"publish_state"`
	NeedJoinIMGroup bool   `json:"need_join_im_group"`
	NeedRTCToken    bool   `json:"need_rtc_token"`
	ServerTimeMS    int64  `json:"server_time_ms"`
}

func currentRoomDataFromProto(resp *roomv1.GetCurrentRoomResponse) currentRoomData {
	if resp == nil {
		return currentRoomData{}
	}

	return currentRoomData{
		HasCurrentRoom:  resp.GetHasCurrentRoom(),
		RoomID:          resp.GetRoomId(),
		RoomVersion:     resp.GetRoomVersion(),
		Role:            resp.GetRole(),
		MicSessionID:    resp.GetMicSessionId(),
		PublishState:    resp.GetPublishState(),
		NeedJoinIMGroup: resp.GetNeedJoinImGroup(),
		NeedRTCToken:    resp.GetNeedRtcToken(),
		ServerTimeMS:    resp.GetServerTimeMs(),
	}
}

// getRoomSnapshot 主动拉取房间页完整快照。
// 该接口只读 Room Cell/snapshot，不刷新 heartbeat，也不隐式 JoinRoom。
func (h *Handler) getRoomSnapshot(writer http.ResponseWriter, request *http.Request) {
	if h.roomQueryClient == nil {
		httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
		return
	}
	roomID := strings.TrimSpace(request.URL.Query().Get("room_id"))
	if !roomid.ValidStringID(roomID) {
		httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
		return
	}

	resp, err := h.roomQueryClient.GetRoomSnapshot(request.Context(), &roomv1.GetRoomSnapshotRequest{
		Meta:         meta(request, roomID, ""),
		RoomId:       roomID,
		ViewerUserId: auth.UserIDFromContext(request.Context()),
	})
	write(writer, request, roomSnapshotDataFromProto(resp), err)
}

// getRoomDetail 返回语音房详情页首屏可直接渲染的数据。
// 它是 GetRoomSnapshot 的 App 聚合版：补 viewer 权限、IM/RTC 入房参数和首屏展示资料。
func (h *Handler) getRoomDetail(writer http.ResponseWriter, request *http.Request) {
	if h.roomQueryClient == nil {
		httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
		return
	}
	roomID := strings.TrimSpace(request.PathValue("room_id"))
	if !roomid.ValidStringID(roomID) {
		httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
		return
	}

	viewerUserID := auth.UserIDFromContext(request.Context())
	resp, err := h.roomQueryClient.GetRoomSnapshot(request.Context(), &roomv1.GetRoomSnapshotRequest{
		Meta:         meta(request, roomID, ""),
		RoomId:       roomID,
		ViewerUserId: viewerUserID,
	})
	if err != nil {
		httpkit.WriteRPCError(writer, request, err)
		return
	}
	profiles := h.roomDisplayProfiles(request, roomInitialProfileUserIDs(resp.GetRoom(), viewerUserID, joinRoomContributionRankLimit))
	httpkit.WriteOK(writer, request, roomDetailDataFromSnapshot(resp, viewerUserID, h.joinRoomRTCData(roomID, viewerUserID), profiles))
}

// listRoomOnlineUsers 分页返回右上角在线人数弹窗需要的用户列表。
func (h *Handler) listRoomOnlineUsers(writer http.ResponseWriter, request *http.Request) {
	if h.roomQueryClient == nil {
		httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
		return
	}
	roomID := strings.TrimSpace(request.PathValue("room_id"))
	if !roomid.ValidStringID(roomID) {
		httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
		return
	}
	page, ok := parsePositiveInt32Query(request, "page", 1)
	if !ok {
		httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
		return
	}
	pageSize, ok := parsePositiveInt32Query(request, "page_size", 50)
	if !ok {
		httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
		return
	}

	resp, err := h.roomQueryClient.ListRoomOnlineUsers(request.Context(), &roomv1.ListRoomOnlineUsersRequest{
		Meta:         meta(request, roomID, ""),
		RoomId:       roomID,
		ViewerUserId: auth.UserIDFromContext(request.Context()),
		Page:         page,
		PageSize:     pageSize,
	})
	if err != nil {
		httpkit.WriteRPCError(writer, request, err)
		return
	}
	userIDs := make([]int64, 0, len(resp.GetUsers()))
	for _, user := range resp.GetUsers() {
		userIDs = append(userIDs, user.GetUserId())
	}
	httpkit.WriteOK(writer, request, roomOnlineUserDataFromProto(resp, h.roomDisplayProfileMap(request, userIDs)))
}

// getRoomGiftPanel 返回房间送礼面板初始化数据。
func (h *Handler) getRoomGiftPanel(writer http.ResponseWriter, request *http.Request) {
	if h.roomQueryClient == nil || h.walletClient == nil {
		httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
		return
	}
	roomID := strings.TrimSpace(request.PathValue("room_id"))
	if !roomid.ValidStringID(roomID) {
		httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
		return
	}

	viewerUserID := auth.UserIDFromContext(request.Context())
	snapshotResp, err := h.roomQueryClient.GetRoomSnapshot(request.Context(), &roomv1.GetRoomSnapshotRequest{
		Meta:         meta(request, roomID, ""),
		RoomId:       roomID,
		ViewerUserId: viewerUserID,
	})
	if err != nil {
		httpkit.WriteRPCError(writer, request, err)
		return
	}
	snapshot := snapshotResp.GetRoom()
	balances, err := h.walletClient.GetBalances(request.Context(), &walletv1.GetBalancesRequest{
		RequestId:  httpkit.RequestIDFromContext(request.Context()),
		UserId:     viewerUserID,
		AssetTypes: []string{"COIN"},
		AppCode:    appcode.FromContext(request.Context()),
	})
	if err != nil {
		httpkit.WriteRPCError(writer, request, err)
		return
	}
	giftResp, err := h.walletClient.ListGiftConfigs(request.Context(), &walletv1.ListGiftConfigsRequest{
		RequestId:    httpkit.RequestIDFromContext(request.Context()),
		AppCode:      appcode.FromContext(request.Context()),
		Page:         1,
		PageSize:     500,
		ActiveOnly:   true,
		RegionId:     snapshot.GetVisibleRegionId(),
		FilterRegion: true,
	})
	if err != nil {
		httpkit.WriteRPCError(writer, request, err)
		return
	}
	gifts := make([]giftConfigData, 0, len(giftResp.GetGifts()))
	for _, gift := range giftResp.GetGifts() {
		gifts = append(gifts, giftFromProto(gift))
	}
	recipientProfiles := h.roomDisplayProfileMap(request, roomGiftRecipientUserIDs(snapshot))
	httpkit.WriteOK(writer, request, roomGiftPanelData{
		CoinBalance:     coinBalanceFromProto(balances),
		Recipients:      roomGiftRecipients(snapshot, recipientProfiles),
		Tabs:            roomGiftTabs(gifts),
		Gifts:           gifts,
		QuantityPresets: []int32{1, 9, 99, 999},
	})
}

// createRoom 把创建房间 HTTP JSON 请求转换为 room-service CreateRoom 命令。
func (h *Handler) createRoom(writer http.ResponseWriter, request *http.Request) {
	var body struct {
		CommandID       string `json:"command_id"`
		SeatCount       int32  `json:"seat_count"`
		Mode            string `json:"mode"`
		RoomName        string `json:"room_name"`
		RoomAvatar      string `json:"room_avatar"`
		RoomDescription string `json:"room_description"`
	}

	if !decode(writer, request, &body) {
		return
	}
	body.CommandID = strings.TrimSpace(body.CommandID)
	body.Mode = strings.TrimSpace(body.Mode)
	body.RoomName = strings.TrimSpace(body.RoomName)
	body.RoomAvatar = strings.TrimSpace(body.RoomAvatar)
	body.RoomDescription = strings.TrimSpace(body.RoomDescription)
	if body.CommandID == "" || body.SeatCount < 0 || body.Mode == "" || body.RoomName == "" {
		// command_id 必须来自客户端用户动作，seat_count 可省略走后台默认值；mode 和 room_name 仍是创建页最小必填信息。
		httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
		return
	}
	if h.userProfileClient == nil {
		// CreateRoom 的 visible_region_id 必须来自 user-service，不能让客户端传入或 gateway 猜测。
		httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
		return
	}
	userResp, err := h.userProfileClient.GetUser(request.Context(), &userv1.GetUserRequest{
		Meta:   authRequestMeta(request, ""),
		UserId: auth.UserIDFromContext(request.Context()),
	})
	if err != nil {
		httpkit.WriteRPCError(writer, request, err)
		return
	}

	roomID := appcode.NewScopedID(appcode.FromContext(request.Context()))
	roomShortID := userResp.GetUser().GetDisplayUserId()
	resp, err := h.roomClient.CreateRoom(request.Context(), &roomv1.CreateRoomRequest{
		Meta:            meta(request, roomID, body.CommandID),
		SeatCount:       body.SeatCount,
		Mode:            body.Mode,
		VisibleRegionId: userResp.GetUser().GetRegionId(),
		RoomName:        body.RoomName,
		RoomAvatar:      body.RoomAvatar,
		RoomDescription: body.RoomDescription,
		RoomShortId:     roomShortID,
	})
	write(writer, request, resp, err)
}

// updateRoomProfile 把 App 房间资料编辑请求转换为 room-service UpdateRoomProfile 命令。
func (h *Handler) updateRoomProfile(writer http.ResponseWriter, request *http.Request) {
	var body struct {
		RoomID          string  `json:"room_id"`
		CommandID       string  `json:"command_id"`
		RoomName        *string `json:"room_name"`
		RoomAvatar      *string `json:"room_avatar"`
		RoomDescription *string `json:"room_description"`
		SeatCount       *int32  `json:"seat_count"`
	}

	if !decode(writer, request, &body) {
		return
	}
	body.RoomID = strings.TrimSpace(body.RoomID)
	body.CommandID = strings.TrimSpace(body.CommandID)
	trimOptionalString(body.RoomName)
	trimOptionalString(body.RoomAvatar)
	trimOptionalString(body.RoomDescription)

	resp, err := h.roomClient.UpdateRoomProfile(request.Context(), &roomv1.UpdateRoomProfileRequest{
		Meta:            meta(request, body.RoomID, body.CommandID),
		RoomName:        body.RoomName,
		RoomAvatar:      body.RoomAvatar,
		RoomDescription: body.RoomDescription,
		SeatCount:       body.SeatCount,
	})
	if err != nil {
		httpkit.WriteRPCError(writer, request, err)
		return
	}
	httpkit.WriteOK(writer, request, updateRoomProfileDataFromProto(resp))
}

func trimOptionalString(value *string) {
	if value == nil {
		return
	}
	*value = strings.TrimSpace(*value)
}

// joinRoom 把进房 HTTP JSON 请求转换为 room-service JoinRoom 命令，并组装房间首屏初始化包。
func (h *Handler) joinRoom(writer http.ResponseWriter, request *http.Request) {
	var body struct {
		RoomID    string `json:"room_id"`
		CommandID string `json:"command_id"`
		Role      string `json:"role"`
	}

	if !decode(writer, request, &body) {
		return
	}

	resp, err := h.roomClient.JoinRoom(request.Context(), &roomv1.JoinRoomRequest{
		Meta: meta(request, body.RoomID, body.CommandID),
		Role: body.Role,
	})
	if err != nil {
		httpkit.WriteRPCError(writer, request, err)
		return
	}
	httpkit.WriteOK(writer, request, h.joinRoomInitialData(request, body.RoomID, resp))
}

func (h *Handler) joinRoomInitialData(request *http.Request, requestedRoomID string, resp *roomv1.JoinRoomResponse) joinRoomData {
	if resp == nil {
		return joinRoomData{}
	}
	viewerUserID := auth.UserIDFromContext(request.Context())
	snapshot := resp.GetRoom()
	roomID := snapshot.GetRoomId()
	if roomID == "" {
		// 老测试桩或异常上游可能只返回 result；用 JoinRoom meta 兜底，避免 HTTP DTO 生成崩溃。
		roomID = strings.TrimSpace(requestedRoomID)
	}

	return joinRoomData{
		Result:           commandResultDataFromProto(resp.GetResult()),
		Room:             roomInitialRoomDataFromSnapshot(snapshot, roomID),
		Viewer:           roomViewerDataFromProto(resp.GetUser(), viewerUserID),
		Seats:            roomSeatDataFromSnapshot(snapshot),
		ContributionRank: roomRankDataFromSnapshot(snapshot, joinRoomContributionRankLimit),
		Profiles:         h.roomInitialProfiles(request, snapshot, viewerUserID),
		IM:               roomIMData{GroupID: roomIMGroupID(roomID), NeedJoinGroup: true},
		RTC:              h.joinRoomRTCData(roomID, viewerUserID),
		ServerTimeMS:     resp.GetResult().GetServerTimeMs(),
	}
}

func (h *Handler) joinRoomRTCData(roomID string, viewerUserID int64) roomRTCData {
	rtc := roomRTCData{NeedToken: true}
	token, err := h.generateTencentRTCToken(viewerUserID, roomID)
	if err != nil {
		// RTC 票据是进房后的音频能力，不应该回滚已经成功的业务 presence。
		rtc.Available = false
		rtc.Reason = "rtc_token_unavailable"
		return rtc
	}
	rtc.Available = true
	rtc.Token = &token
	return rtc
}

func (h *Handler) roomInitialProfiles(request *http.Request, snapshot *roomv1.RoomSnapshot, viewerUserID int64) []roomUserProfileData {
	if h.userProfileClient == nil {
		return nil
	}
	userIDs := roomInitialProfileUserIDs(snapshot, viewerUserID, joinRoomContributionRankLimit)
	if len(userIDs) == 0 {
		return nil
	}
	resp, err := h.userProfileClient.BatchGetUsers(request.Context(), &userv1.BatchGetUsersRequest{
		Meta:    authRequestMeta(request, ""),
		UserIds: userIDs,
	})
	if err != nil {
		// 用户资料只服务首屏展示，失败时让客户端用 /users/profiles:batch 补偿，避免阻塞进房。
		return nil
	}

	profiles := make([]roomUserProfileData, 0, len(userIDs))
	for _, userID := range userIDs {
		user := resp.GetUsers()[userID]
		if user == nil {
			continue
		}
		profiles = append(profiles, roomUserProfileData{
			UserID:        strconv.FormatInt(user.GetUserId(), 10),
			DisplayUserID: user.GetDisplayUserId(),
			Username:      user.GetUsername(),
			Avatar:        user.GetAvatar(),
		})
	}
	return profiles
}

// roomHeartbeat 把客户端显式心跳转换为 room-service presence refresh 命令。
func (h *Handler) roomHeartbeat(writer http.ResponseWriter, request *http.Request) {
	var body struct {
		RoomID    string `json:"room_id"`
		CommandID string `json:"command_id"`
	}

	if !decode(writer, request, &body) {
		return
	}

	resp, err := h.roomClient.RoomHeartbeat(request.Context(), &roomv1.RoomHeartbeatRequest{
		Meta: meta(request, body.RoomID, body.CommandID),
	})
	write(writer, request, resp, err)
}

// leaveRoom 把退房 HTTP JSON 请求转换为 room-service LeaveRoom 命令。
func (h *Handler) leaveRoom(writer http.ResponseWriter, request *http.Request) {
	var body struct {
		RoomID    string `json:"room_id"`
		CommandID string `json:"command_id"`
	}

	if !decode(writer, request, &body) {
		return
	}

	resp, err := h.roomClient.LeaveRoom(request.Context(), &roomv1.LeaveRoomRequest{
		Meta: meta(request, body.RoomID, body.CommandID),
	})
	write(writer, request, resp, err)
}

// closeRoom 是设计稿右上角电源按钮入口：这里只退出当前用户，不关闭房间生命周期。
func (h *Handler) closeRoom(writer http.ResponseWriter, request *http.Request) {
	h.leaveRoom(writer, request)
}

// micUp 把上麦 HTTP JSON 请求转换为 room-service MicUp 命令。
func (h *Handler) micUp(writer http.ResponseWriter, request *http.Request) {
	var body struct {
		RoomID    string `json:"room_id"`
		CommandID string `json:"command_id"`
		SeatNo    int32  `json:"seat_no"`
	}

	if !decode(writer, request, &body) {
		return
	}

	resp, err := h.roomClient.MicUp(request.Context(), &roomv1.MicUpRequest{
		Meta:   meta(request, body.RoomID, body.CommandID),
		SeatNo: body.SeatNo,
	})
	write(writer, request, resp, err)
}

// micDown 把下麦 HTTP JSON 请求转换为 room-service MicDown 命令。
func (h *Handler) micDown(writer http.ResponseWriter, request *http.Request) {
	var body struct {
		RoomID       string `json:"room_id"`
		CommandID    string `json:"command_id"`
		TargetUserID int64  `json:"target_user_id"`
		Reason       string `json:"reason"`
	}

	if !decode(writer, request, &body) {
		return
	}

	resp, err := h.roomClient.MicDown(request.Context(), &roomv1.MicDownRequest{
		Meta:         meta(request, body.RoomID, body.CommandID),
		TargetUserId: body.TargetUserID,
		Reason:       body.Reason,
	})
	write(writer, request, resp, err)
}

// changeMicSeat 把换麦位 HTTP JSON 请求转换为 room-service ChangeMicSeat 命令。
func (h *Handler) changeMicSeat(writer http.ResponseWriter, request *http.Request) {
	var body struct {
		RoomID       string `json:"room_id"`
		CommandID    string `json:"command_id"`
		TargetUserID int64  `json:"target_user_id"`
		SeatNo       int32  `json:"seat_no"`
	}

	if !decode(writer, request, &body) {
		return
	}

	resp, err := h.roomClient.ChangeMicSeat(request.Context(), &roomv1.ChangeMicSeatRequest{
		Meta:         meta(request, body.RoomID, body.CommandID),
		TargetUserId: body.TargetUserID,
		SeatNo:       body.SeatNo,
	})
	write(writer, request, resp, err)
}

// confirmMicPublishing 把客户端 SDK 发流成功回调转换为 room-service 发流确认命令。
func (h *Handler) confirmMicPublishing(writer http.ResponseWriter, request *http.Request) {
	var body struct {
		RoomID       string `json:"room_id"`
		CommandID    string `json:"command_id"`
		TargetUserID int64  `json:"target_user_id"`
		MicSessionID string `json:"mic_session_id"`
		RoomVersion  int64  `json:"room_version"`
		EventTimeMS  int64  `json:"event_time_ms"`
		Source       string `json:"source"`
	}

	if !decode(writer, request, &body) {
		return
	}

	resp, err := h.roomClient.ConfirmMicPublishing(request.Context(), &roomv1.ConfirmMicPublishingRequest{
		Meta:         meta(request, body.RoomID, body.CommandID),
		TargetUserId: body.TargetUserID,
		MicSessionId: body.MicSessionID,
		RoomVersion:  body.RoomVersion,
		EventTimeMs:  body.EventTimeMS,
		Source:       body.Source,
	})
	write(writer, request, resp, err)
}

// setMicMute 把麦克风静音 HTTP 请求转换为 room-service SetMicMute 命令。
func (h *Handler) setMicMute(writer http.ResponseWriter, request *http.Request) {
	var body struct {
		RoomID       string `json:"room_id"`
		CommandID    string `json:"command_id"`
		TargetUserID int64  `json:"target_user_id"`
		Muted        bool   `json:"muted"`
	}

	if !decode(writer, request, &body) {
		return
	}

	resp, err := h.roomClient.SetMicMute(request.Context(), &roomv1.SetMicMuteRequest{
		Meta:         meta(request, body.RoomID, body.CommandID),
		TargetUserId: body.TargetUserID,
		Muted:        body.Muted,
	})
	write(writer, request, resp, err)
}

// setMicSeatLock 把锁麦 HTTP JSON 请求转换为 room-service SetMicSeatLock 命令。
func (h *Handler) setMicSeatLock(writer http.ResponseWriter, request *http.Request) {
	var body struct {
		RoomID    string `json:"room_id"`
		CommandID string `json:"command_id"`
		SeatNo    int32  `json:"seat_no"`
		Locked    bool   `json:"locked"`
	}

	if !decode(writer, request, &body) {
		return
	}

	resp, err := h.roomClient.SetMicSeatLock(request.Context(), &roomv1.SetMicSeatLockRequest{
		Meta:   meta(request, body.RoomID, body.CommandID),
		SeatNo: body.SeatNo,
		Locked: body.Locked,
	})
	write(writer, request, resp, err)
}

// setChatEnabled 把公屏开关 HTTP JSON 请求转换为 room-service SetChatEnabled 命令。
func (h *Handler) setChatEnabled(writer http.ResponseWriter, request *http.Request) {
	var body struct {
		RoomID    string `json:"room_id"`
		CommandID string `json:"command_id"`
		Enabled   bool   `json:"enabled"`
	}

	if !decode(writer, request, &body) {
		return
	}

	resp, err := h.roomClient.SetChatEnabled(request.Context(), &roomv1.SetChatEnabledRequest{
		Meta:    meta(request, body.RoomID, body.CommandID),
		Enabled: body.Enabled,
	})
	write(writer, request, resp, err)
}

// setRoomAdmin 把管理员增删 HTTP JSON 请求转换为 room-service SetRoomAdmin 命令。
func (h *Handler) setRoomAdmin(writer http.ResponseWriter, request *http.Request) {
	var body struct {
		RoomID       string `json:"room_id"`
		CommandID    string `json:"command_id"`
		TargetUserID int64  `json:"target_user_id"`
		Enabled      bool   `json:"enabled"`
	}

	if !decode(writer, request, &body) {
		return
	}

	resp, err := h.roomClient.SetRoomAdmin(request.Context(), &roomv1.SetRoomAdminRequest{
		Meta:         meta(request, body.RoomID, body.CommandID),
		TargetUserId: body.TargetUserID,
		Enabled:      body.Enabled,
	})
	write(writer, request, resp, err)
}

// muteUser 把禁言 HTTP JSON 请求转换为 room-service MuteUser 命令。
func (h *Handler) muteUser(writer http.ResponseWriter, request *http.Request) {
	var body struct {
		RoomID       string `json:"room_id"`
		CommandID    string `json:"command_id"`
		TargetUserID int64  `json:"target_user_id"`
		Muted        bool   `json:"muted"`
	}

	if !decode(writer, request, &body) {
		return
	}

	resp, err := h.roomClient.MuteUser(request.Context(), &roomv1.MuteUserRequest{
		Meta:         meta(request, body.RoomID, body.CommandID),
		TargetUserId: body.TargetUserID,
		Muted:        body.Muted,
	})
	write(writer, request, resp, err)
}

// kickUser 把踢人 HTTP JSON 请求转换为 room-service KickUser 命令。
func (h *Handler) kickUser(writer http.ResponseWriter, request *http.Request) {
	var body struct {
		RoomID       string `json:"room_id"`
		CommandID    string `json:"command_id"`
		TargetUserID int64  `json:"target_user_id"`
	}

	if !decode(writer, request, &body) {
		return
	}

	resp, err := h.roomClient.KickUser(request.Context(), &roomv1.KickUserRequest{
		Meta:         meta(request, body.RoomID, body.CommandID),
		TargetUserId: body.TargetUserID,
	})
	write(writer, request, resp, err)
}

// unbanUser 把解封 HTTP JSON 请求转换为 room-service UnbanUser 命令。
func (h *Handler) unbanUser(writer http.ResponseWriter, request *http.Request) {
	var body struct {
		RoomID       string `json:"room_id"`
		CommandID    string `json:"command_id"`
		TargetUserID int64  `json:"target_user_id"`
	}

	if !decode(writer, request, &body) {
		return
	}

	resp, err := h.roomClient.UnbanUser(request.Context(), &roomv1.UnbanUserRequest{
		Meta:         meta(request, body.RoomID, body.CommandID),
		TargetUserId: body.TargetUserID,
	})
	write(writer, request, resp, err)
}

// sendGift 把送礼 HTTP JSON 请求转换为 room-service SendGift 命令。
// 扣费、幂等、房间表现和 outbox 仍由 room-service 的命令链路统一处理。
func (h *Handler) sendGift(writer http.ResponseWriter, request *http.Request) {
	var body struct {
		RoomID        string  `json:"room_id"`
		CommandID     string  `json:"command_id"`
		TargetType    string  `json:"target_type"`
		TargetUserID  int64   `json:"target_user_id"`
		TargetUserIDs []int64 `json:"target_user_ids"`
		GiftID        string  `json:"gift_id"`
		GiftCount     int32   `json:"gift_count"`
	}

	if !decode(writer, request, &body) {
		return
	}
	targetType := strings.TrimSpace(body.TargetType)
	if targetType == "" {
		targetType = "user"
	}
	targetUserIDs := normalizeGiftTargetUserIDs(body.TargetUserID, body.TargetUserIDs)
	if targetType == "user" && len(targetUserIDs) != 1 {
		httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
		return
	}

	resp, err := h.roomClient.SendGift(request.Context(), &roomv1.SendGiftRequest{
		Meta:          meta(request, body.RoomID, body.CommandID),
		TargetType:    targetType,
		TargetUserIds: targetUserIDs,
		TargetUserId:  firstUserID(targetUserIDs),
		GiftId:        body.GiftID,
		GiftCount:     body.GiftCount,
	})
	write(writer, request, resp, err)
}

// batchRoomDisplayProfiles 返回房间和公屏专用展示资料。
func (h *Handler) batchRoomDisplayProfiles(writer http.ResponseWriter, request *http.Request) {
	if h.userProfileClient == nil {
		httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
		return
	}
	userIDs, ok := parseBatchUserIDs(request)
	if !ok {
		httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
		return
	}
	httpkit.WriteOK(writer, request, map[string]any{"profiles": h.roomDisplayProfiles(request, userIDs)})
}

func (h *Handler) roomDisplayProfiles(request *http.Request, userIDs []int64) []roomDisplayProfileData {
	profileMap := h.roomDisplayProfileMap(request, userIDs)
	profiles := make([]roomDisplayProfileData, 0, len(userIDs))
	for _, userID := range userIDs {
		if profile, ok := profileMap[userID]; ok {
			profiles = append(profiles, profile)
		}
	}
	return profiles
}

func (h *Handler) roomDisplayProfileMap(request *http.Request, userIDs []int64) map[int64]roomDisplayProfileData {
	if h.userProfileClient == nil {
		return nil
	}
	userIDs = uniquePositiveUserIDs(userIDs)
	if len(userIDs) == 0 {
		return nil
	}
	resp, err := h.userProfileClient.BatchGetUsers(request.Context(), &userv1.BatchGetUsersRequest{
		Meta:    authRequestMeta(request, ""),
		UserIds: userIDs,
	})
	if err != nil {
		// 房间资料是展示增强，失败时保留主房间数据，让客户端后续重试 batch 接口。
		return nil
	}
	profiles := make(map[int64]roomDisplayProfileData, len(resp.GetUsers()))
	for _, userID := range userIDs {
		user := resp.GetUsers()[userID]
		if user == nil {
			continue
		}
		profiles[userID] = roomDisplayProfileData{
			UserID:        formatOptionalUserID(user.GetUserId()),
			Username:      user.GetUsername(),
			Avatar:        user.GetAvatar(),
			DisplayUserID: user.GetDisplayUserId(),
			VIP:           map[string]any{},
			Level:         map[string]any{},
			Badges:        []map[string]any{},
			AvatarFrame:   map[string]any{},
			Vehicle:       map[string]any{},
			Charm:         0,
		}
	}
	return profiles
}

func roomGiftRecipientUserIDs(snapshot *roomv1.RoomSnapshot) []int64 {
	userIDs := make([]int64, 0, len(snapshot.GetMicSeats())+1)
	for _, seat := range snapshot.GetMicSeats() {
		if seat.GetUserId() > 0 {
			userIDs = append(userIDs, seat.GetUserId())
		}
	}
	if len(userIDs) == 0 {
		userIDs = append(userIDs, snapshot.GetOwnerUserId())
	}
	return uniquePositiveUserIDs(userIDs)
}

func roomGiftRecipients(snapshot *roomv1.RoomSnapshot, profiles map[int64]roomDisplayProfileData) []roomGiftRecipientData {
	userIDs := roomGiftRecipientUserIDs(snapshot)
	recipients := make([]roomGiftRecipientData, 0, len(userIDs)+1)
	if len(userIDs) > 1 {
		recipients = append(recipients, roomGiftRecipientData{TargetType: "all_mic", Label: "all_mic"})
	}
	seatByUser := make(map[int64]int32, len(snapshot.GetMicSeats()))
	for _, seat := range snapshot.GetMicSeats() {
		if seat.GetUserId() > 0 {
			seatByUser[seat.GetUserId()] = seat.GetSeatNo()
		}
	}
	for _, userID := range userIDs {
		recipient := roomGiftRecipientData{
			TargetType: "user",
			UserID:     formatOptionalUserID(userID),
			SeatNo:     seatByUser[userID],
		}
		if profile, ok := profiles[userID]; ok {
			copied := profile
			recipient.Profile = &copied
			recipient.Label = copied.Username
		}
		recipients = append(recipients, recipient)
	}
	return recipients
}

func roomGiftTabs(gifts []giftConfigData) []roomGiftTabData {
	tabs := []roomGiftTabData{{Key: "all", Order: 0}}
	seen := map[string]bool{"all": true}
	for _, gift := range gifts {
		key := strings.TrimSpace(gift.GiftTypeCode)
		if key == "" || seen[key] {
			continue
		}
		seen[key] = true
		tabs = append(tabs, roomGiftTabData{Key: key, Order: int32(len(tabs) * 10)})
	}
	return tabs
}

func coinBalanceFromProto(resp *walletv1.GetBalancesResponse) int64 {
	for _, balance := range resp.GetBalances() {
		if balance.GetAssetType() == "COIN" {
			return balance.GetAvailableAmount()
		}
	}
	return 0
}

func uniquePositiveUserIDs(userIDs []int64) []int64 {
	seen := make(map[int64]bool, len(userIDs))
	result := make([]int64, 0, len(userIDs))
	for _, userID := range userIDs {
		if userID <= 0 || seen[userID] {
			continue
		}
		seen[userID] = true
		result = append(result, userID)
	}
	return result
}

func normalizeGiftTargetUserIDs(targetUserID int64, targetUserIDs []int64) []int64 {
	result := uniquePositiveUserIDs(targetUserIDs)
	if len(result) == 0 && targetUserID > 0 {
		result = append(result, targetUserID)
	}
	return result
}

func firstUserID(userIDs []int64) int64 {
	if len(userIDs) == 0 {
		return 0
	}
	return userIDs[0]
}
