package walletapi

import (
	"fmt"
	"net/http"
	"strconv"
	"strings"
	"time"

	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/imgroup"
	"hyapp/pkg/roomid"
	"hyapp/pkg/xerr"
	"hyapp/services/gateway-service/internal/auth"
	"hyapp/services/gateway-service/internal/transport/http/httpkit"
)

type redPacketConfigData struct {
	AppCode            string  `json:"app_code"`
	Enabled            bool    `json:"enabled"`
	CountTiers         []int32 `json:"count_tiers"`
	AmountTiers        []int64 `json:"amount_tiers"`
	DelayedOpenSeconds int32   `json:"delayed_open_seconds"`
	ExpireSeconds      int32   `json:"expire_seconds"`
	DailySendLimit     int32   `json:"daily_send_limit"`
	UpdatedByAdminID   int64   `json:"updated_by_admin_id"`
	RuleURL            string  `json:"rule_url"`
	CreatedAtMS        int64   `json:"created_at_ms"`
	UpdatedAtMS        int64   `json:"updated_at_ms"`
}

type redPacketData struct {
	AppCode           string               `json:"app_code"`
	PacketID          string               `json:"packet_id"`
	CommandID         string               `json:"command_id"`
	SenderUserID      int64                `json:"sender_user_id"`
	RoomID            string               `json:"room_id"`
	RegionID          int64                `json:"region_id"`
	PacketType        string               `json:"packet_type"`
	TotalAmount       int64                `json:"total_amount"`
	PacketCount       int32                `json:"packet_count"`
	RemainingAmount   int64                `json:"remaining_amount"`
	RemainingCount    int32                `json:"remaining_count"`
	Status            string               `json:"status"`
	OpenAtMS          int64                `json:"open_at_ms"`
	ExpiresAtMS       int64                `json:"expires_at_ms"`
	CreatedAtMS       int64                `json:"created_at_ms"`
	UpdatedAtMS       int64                `json:"updated_at_ms"`
	ClaimedByViewer   bool                 `json:"claimed_by_viewer"`
	ViewerClaimAmount int64                `json:"viewer_claim_amount"`
	Claims            []redPacketClaimData `json:"claims,omitempty"`
}

type redPacketClaimData struct {
	AppCode             string `json:"app_code"`
	ClaimID             string `json:"claim_id"`
	CommandID           string `json:"command_id"`
	PacketID            string `json:"packet_id"`
	UserID              int64  `json:"user_id"`
	Amount              int64  `json:"amount"`
	WalletTransactionID string `json:"wallet_transaction_id"`
	Status              string `json:"status"`
	FailureReason       string `json:"failure_reason,omitempty"`
	CreatedAtMS         int64  `json:"created_at_ms"`
	UpdatedAtMS         int64  `json:"updated_at_ms"`
}

type createRedPacketRequestBody struct {
	CommandID        string `json:"command_id"`
	CommandIDCamel   string `json:"commandId"`
	PacketType       string `json:"packet_type"`
	PacketTypeCamel  string `json:"packetType"`
	TotalAmount      int64  `json:"total_amount"`
	TotalAmountCamel int64  `json:"totalAmount"`
	PacketCount      int32  `json:"packet_count"`
	PacketCountCamel int32  `json:"packetCount"`
}

func (b createRedPacketRequestBody) normalizedCommandID() string {
	if commandID := strings.TrimSpace(b.CommandID); commandID != "" {
		return commandID
	}
	return strings.TrimSpace(b.CommandIDCamel)
}

func (b createRedPacketRequestBody) normalizedPacketType() string {
	if packetType := strings.TrimSpace(b.PacketType); packetType != "" {
		return packetType
	}
	return strings.TrimSpace(b.PacketTypeCamel)
}

func (b createRedPacketRequestBody) normalizedTotalAmount() int64 {
	if b.TotalAmount > 0 {
		return b.TotalAmount
	}
	return b.TotalAmountCamel
}

func (b createRedPacketRequestBody) normalizedPacketCount() int32 {
	if b.PacketCount > 0 {
		return b.PacketCount
	}
	return b.PacketCountCamel
}

type claimRedPacketRequestBody struct {
	CommandID      string `json:"command_id"`
	CommandIDCamel string `json:"commandId"`
}

func (b claimRedPacketRequestBody) normalizedCommandID() string {
	if commandID := strings.TrimSpace(b.CommandID); commandID != "" {
		return commandID
	}
	return strings.TrimSpace(b.CommandIDCamel)
}

type voiceRoomRedPacketSendBody struct {
	RoomID        string `json:"roomId"`
	RoomIDSnake   string `json:"room_id"`
	PacketMode    string `json:"packetMode"`
	PacketType    string `json:"packet_type"`
	TotalAmount   int64  `json:"totalAmount"`
	TotalAmountV2 int64  `json:"total_amount"`
	TotalCount    int32  `json:"totalCount"`
	PacketCount   int32  `json:"packet_count"`
	RequestID     string `json:"requestId"`
	RequestIDV2   string `json:"request_id"`
}

func (b voiceRoomRedPacketSendBody) roomID() string {
	if value := strings.TrimSpace(b.RoomID); value != "" {
		return value
	}
	return strings.TrimSpace(b.RoomIDSnake)
}

func (b voiceRoomRedPacketSendBody) mode() string {
	if value := strings.TrimSpace(b.PacketMode); value != "" {
		return value
	}
	return strings.TrimSpace(b.PacketType)
}

func (b voiceRoomRedPacketSendBody) amount() int64 {
	if b.TotalAmount > 0 {
		return b.TotalAmount
	}
	return b.TotalAmountV2
}

func (b voiceRoomRedPacketSendBody) count() int32 {
	if b.TotalCount > 0 {
		return b.TotalCount
	}
	return b.PacketCount
}

func (b voiceRoomRedPacketSendBody) requestID() string {
	if value := strings.TrimSpace(b.RequestID); value != "" {
		return value
	}
	return strings.TrimSpace(b.RequestIDV2)
}

type voiceRoomRedPacketClaimBody struct {
	PacketNo  string `json:"packetNo"`
	PacketID  string `json:"packet_id"`
	RequestID string `json:"requestId"`
}

func (b voiceRoomRedPacketClaimBody) packetNo() string {
	if value := strings.TrimSpace(b.PacketNo); value != "" {
		return value
	}
	return strings.TrimSpace(b.PacketID)
}

func (h *Handler) getCurrentRegionIMGroup(writer http.ResponseWriter, request *http.Request) {
	if h.userProfileClient == nil {
		httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
		return
	}
	user, ok := h.currentUser(writer, request)
	if !ok {
		return
	}
	if user.GetRegionId() <= 0 {
		httpkit.WriteError(writer, request, http.StatusConflict, "voice_room_region_missing", "voice room region is missing")
		return
	}
	groupID, err := imgroup.RegionBroadcastGroupIDWithPrefix(h.imGroupIDPrefix, appcode.FromContext(request.Context()), user.GetRegionId())
	if err != nil {
		httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
		return
	}
	httpkit.WriteOK(writer, request, map[string]any{
		"regionId":   user.GetRegionId(),
		"regionCode": user.GetRegionCode(),
		"regionName": user.GetRegionName(),
		"groupId":    groupID,
		"groupName":  groupID,
	})
}

func (h *Handler) sendVoiceRoomRedPacket(writer http.ResponseWriter, request *http.Request) {
	if h.walletClient == nil || h.roomQueryClient == nil || h.userProfileClient == nil {
		httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
		return
	}
	var body voiceRoomRedPacketSendBody
	if !httpkit.Decode(writer, request, &body) {
		return
	}
	roomID := body.roomID()
	packetType := internalRedPacketType(body.mode())
	requestID := body.requestID()
	if !roomid.ValidStringID(roomID) || packetType == "" || body.amount() <= 0 || body.count() <= 0 || requestID == "" {
		httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
		return
	}
	user, ok := h.currentUser(writer, request)
	if !ok {
		return
	}
	if user.GetRegionId() <= 0 {
		httpkit.WriteError(writer, request, http.StatusConflict, "voice_room_region_missing", "voice room region is missing")
		return
	}
	snapshotResp, err := h.roomQueryClient.GetRoomSnapshot(request.Context(), &roomv1.GetRoomSnapshotRequest{
		Meta:         httpkit.RoomMeta(request, roomID, ""),
		RoomId:       roomID,
		ViewerUserId: auth.UserIDFromContext(request.Context()),
	})
	if err != nil {
		writeRedPacketRPCError(writer, request, err)
		return
	}
	regionID := snapshotResp.GetRoom().GetVisibleRegionId()
	if regionID <= 0 {
		httpkit.WriteError(writer, request, http.StatusConflict, "voice_room_region_missing", "voice room region is missing")
		return
	}
	if regionID != user.GetRegionId() {
		httpkit.WriteError(writer, request, http.StatusForbidden, "voice_room_region_not_match", "permission denied")
		return
	}
	commandID := fmt.Sprintf("red_packet_send:%d:%s", auth.UserIDFromContext(request.Context()), requestID)
	resp, err := h.walletClient.CreateRedPacket(request.Context(), &walletv1.CreateRedPacketRequest{
		RequestId:    requestID,
		AppCode:      appcode.FromContext(request.Context()),
		CommandId:    commandID,
		SenderUserId: auth.UserIDFromContext(request.Context()),
		RoomId:       roomID,
		RegionId:     regionID,
		PacketType:   packetType,
		TotalAmount:  body.amount(),
		PacketCount:  body.count(),
		RoomLocked:   snapshotResp.GetRoom().GetLocked(),
		OwnerUserId:  snapshotResp.GetRoom().GetOwnerUserId(),
	})
	if err != nil {
		writeRedPacketRPCError(writer, request, err)
		return
	}
	httpkit.WriteOK(writer, request, map[string]any{
		"packet":       voiceRoomPacketFromProtoWithSender(resp.GetPacket(), user),
		"balanceAfter": resp.GetBalanceAfter(),
		"serverTime":   resp.GetServerTimeMs(),
	})
}

func (h *Handler) listActiveRedPackets(writer http.ResponseWriter, request *http.Request) {
	if h.walletClient == nil || h.userProfileClient == nil {
		httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
		return
	}
	roomID := strings.TrimSpace(request.URL.Query().Get("roomId"))
	if roomID == "" {
		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.walletClient.ListRedPackets(request.Context(), &walletv1.ListRedPacketsRequest{
		AppCode:      appcode.FromContext(request.Context()),
		RoomId:       roomID,
		ViewerUserId: auth.UserIDFromContext(request.Context()),
		Page:         1,
		PageSize:     100,
	})
	if err != nil {
		writeRedPacketRPCError(writer, request, err)
		return
	}
	activePackets := make([]*walletv1.RedPacket, 0, len(resp.GetPackets()))
	serverTimeMS := resp.GetServerTimeMs()
	if serverTimeMS <= 0 {
		serverTimeMS = time.Now().UTC().UnixMilli()
	}
	for _, packet := range resp.GetPackets() {
		statusActive := packet.GetStatus() == "active" || packet.GetStatus() == "waiting_open"
		// active 是“当前用户仍可领取”列表：状态、有效期、剩余份数和用户领取态必须同时满足。
		if !statusActive || packet.GetExpiresAtMs() <= serverTimeMS || packet.GetRemainingCount() <= 0 || packet.GetRemainingAmount() <= 0 || packet.GetClaimedByViewer() {
			continue
		}
		activePackets = append(activePackets, packet)
	}
	profiles, err := h.redPacketSenderProfiles(request, activePackets)
	if err != nil {
		httpkit.WriteRPCError(writer, request, err)
		return
	}
	packets := make([]map[string]any, 0, len(activePackets))
	for _, packet := range activePackets {
		packets = append(packets, voiceRoomPacketFromProtoWithSender(packet, profiles[packet.GetSenderUserId()]))
	}
	httpkit.WriteOK(writer, request, map[string]any{
		"packets":    packets,
		"serverTime": resp.GetServerTimeMs(),
	})
}

func (h *Handler) claimVoiceRoomRedPacket(writer http.ResponseWriter, request *http.Request) {
	if h.walletClient == nil || h.roomGuardClient == nil || h.userProfileClient == nil {
		httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
		return
	}
	var body voiceRoomRedPacketClaimBody
	if !httpkit.Decode(writer, request, &body) {
		return
	}
	packetNo := body.packetNo()
	if packetNo == "" {
		httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
		return
	}
	packetResp, err := h.walletClient.GetRedPacket(request.Context(), &walletv1.GetRedPacketRequest{
		AppCode:      appcode.FromContext(request.Context()),
		PacketId:     packetNo,
		ViewerUserId: auth.UserIDFromContext(request.Context()),
	})
	if err != nil {
		writeRedPacketRPCError(writer, request, err)
		return
	}
	packet := packetResp.GetPacket()
	if !h.verifyRedPacketRoomPresence(writer, request, packet.GetRoomId()) {
		return
	}
	user, ok := h.currentUser(writer, request)
	if !ok {
		return
	}
	if user.GetRegionId() <= 0 {
		httpkit.WriteError(writer, request, http.StatusConflict, "voice_room_region_missing", "voice room region is missing")
		return
	}
	if packet.GetRegionId() > 0 && packet.GetRegionId() != user.GetRegionId() {
		httpkit.WriteError(writer, request, http.StatusForbidden, "voice_room_region_not_match", "permission denied")
		return
	}
	sender, err := h.redPacketSenderProfile(request, packet, user)
	if err != nil {
		httpkit.WriteRPCError(writer, request, err)
		return
	}
	commandID := fmt.Sprintf("red_packet_claim:%s:%d", packetNo, auth.UserIDFromContext(request.Context()))
	resp, err := h.walletClient.ClaimRedPacket(request.Context(), &walletv1.ClaimRedPacketRequest{
		AppCode:   appcode.FromContext(request.Context()),
		CommandId: commandID,
		PacketId:  packetNo,
		UserId:    auth.UserIDFromContext(request.Context()),
	})
	if err != nil {
		writeRedPacketRPCError(writer, request, err)
		return
	}
	httpkit.WriteOK(writer, request, map[string]any{
		"claim":        voiceRoomClaimFromProtoWithUser(resp.GetClaim(), user),
		"packet":       voiceRoomPacketFromProtoWithSender(resp.GetPacket(), sender),
		"balanceAfter": resp.GetBalanceAfter(),
		"serverTime":   resp.GetServerTimeMs(),
	})
}

func (h *Handler) getVoiceRoomRedPacket(writer http.ResponseWriter, request *http.Request) {
	if h.walletClient == nil || h.userProfileClient == nil {
		httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
		return
	}
	packetNo := strings.TrimSpace(request.URL.Query().Get("packetNo"))
	if packetNo == "" {
		packetNo = strings.TrimSpace(request.URL.Query().Get("packet_id"))
	}
	if packetNo == "" {
		httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
		return
	}
	resp, err := h.walletClient.GetRedPacket(request.Context(), &walletv1.GetRedPacketRequest{
		AppCode:       appcode.FromContext(request.Context()),
		PacketId:      packetNo,
		ViewerUserId:  auth.UserIDFromContext(request.Context()),
		IncludeClaims: true,
	})
	if err != nil {
		writeRedPacketRPCError(writer, request, err)
		return
	}
	sender, err := h.redPacketSenderProfile(request, resp.GetPacket(), nil)
	if err != nil {
		httpkit.WriteRPCError(writer, request, err)
		return
	}
	httpkit.WriteOK(writer, request, map[string]any{
		"packet":     voiceRoomPacketFromProtoWithSender(resp.GetPacket(), sender),
		"serverTime": resp.GetServerTimeMs(),
	})
}

func (h *Handler) listRedPacketClaims(writer http.ResponseWriter, request *http.Request) {
	if h.walletClient == nil || h.userProfileClient == nil {
		httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
		return
	}
	packetNo := strings.TrimSpace(request.URL.Query().Get("packetNo"))
	if packetNo == "" {
		packetNo = strings.TrimSpace(request.URL.Query().Get("packet_id"))
	}
	if packetNo == "" {
		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, "pageSize", 20)
	if !ok {
		pageSize, ok = httpkit.PositiveInt32Query(request, "page_size", 20)
	}
	if !ok {
		httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
		return
	}
	resp, err := h.walletClient.GetRedPacket(request.Context(), &walletv1.GetRedPacketRequest{
		AppCode:       appcode.FromContext(request.Context()),
		PacketId:      packetNo,
		ViewerUserId:  auth.UserIDFromContext(request.Context()),
		IncludeClaims: true,
	})
	if err != nil {
		writeRedPacketRPCError(writer, request, err)
		return
	}
	all := resp.GetPacket().GetClaims()
	start := int((page - 1) * pageSize)
	end := start + int(pageSize)
	if start > len(all) {
		start = len(all)
	}
	if end > len(all) {
		end = len(all)
	}
	profiles, err := h.redPacketClaimProfiles(request, all[start:end])
	if err != nil {
		httpkit.WriteRPCError(writer, request, err)
		return
	}
	items := make([]map[string]any, 0, end-start)
	for _, claim := range all[start:end] {
		items = append(items, voiceRoomClaimFromProtoWithUser(claim, profiles[claim.GetUserId()]))
	}
	httpkit.WriteOK(writer, request, map[string]any{
		"items":      items,
		"total":      len(all),
		"page":       page,
		"pageSize":   pageSize,
		"serverTime": resp.GetServerTimeMs(),
	})
}

func (h *Handler) redPacketClaimProfiles(request *http.Request, claims []*walletv1.RedPacketClaim) (map[int64]*userv1.User, error) {
	userIDs := make([]int64, 0, len(claims))
	seen := make(map[int64]struct{}, len(claims))
	for _, claim := range claims {
		userID := claim.GetUserId()
		if userID <= 0 {
			continue
		}
		if _, exists := seen[userID]; exists {
			continue
		}
		seen[userID] = struct{}{}
		userIDs = append(userIDs, userID)
	}
	if len(userIDs) == 0 {
		return map[int64]*userv1.User{}, nil
	}
	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 (h *Handler) redPacketSenderProfiles(request *http.Request, packets []*walletv1.RedPacket) (map[int64]*userv1.User, error) {
	userIDs := make([]int64, 0, len(packets))
	seen := make(map[int64]struct{}, len(packets))
	for _, packet := range packets {
		userID := packet.GetSenderUserId()
		if userID <= 0 {
			continue
		}
		if _, exists := seen[userID]; exists {
			continue
		}
		seen[userID] = struct{}{}
		userIDs = append(userIDs, userID)
	}
	if len(userIDs) == 0 {
		return map[int64]*userv1.User{}, nil
	}
	// active 列表可能有多个红包，发送者展示资料必须批量拉取，避免房间侧边栏刷新造成 N+1 RPC。
	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 (h *Handler) redPacketSenderProfile(request *http.Request, packet *walletv1.RedPacket, currentUser *userv1.User) (*userv1.User, error) {
	senderUserID := packet.GetSenderUserId()
	if senderUserID <= 0 {
		return nil, nil
	}
	// 发送者资料属于 user-service；claim 流程复用当前用户资料，避免同一用户发抢自己的红包时重复 RPC。
	if currentUser != nil && currentUser.GetUserId() == senderUserID {
		return currentUser, nil
	}
	resp, err := h.userProfileClient.GetUser(request.Context(), &userv1.GetUserRequest{
		Meta:   httpkit.UserMeta(request, ""),
		UserId: senderUserID,
	})
	if err != nil {
		return nil, err
	}
	return resp.GetUser(), nil
}

func (h *Handler) listSentRedPackets(writer http.ResponseWriter, request *http.Request) {
	if h.walletClient == nil {
		httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
		return
	}
	page, ok := httpkit.PositiveInt32Query(request, "page", 1)
	if !ok {
		httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
		return
	}
	pageSize, ok := httpkit.PositiveInt32Query(request, "pageSize", 20)
	if !ok {
		pageSize, ok = httpkit.PositiveInt32Query(request, "page_size", 20)
	}
	if !ok {
		httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
		return
	}
	resp, err := h.walletClient.ListRedPackets(request.Context(), &walletv1.ListRedPacketsRequest{
		AppCode:      appcode.FromContext(request.Context()),
		SenderUserId: auth.UserIDFromContext(request.Context()),
		Page:         page,
		PageSize:     pageSize,
	})
	if err != nil {
		writeRedPacketRPCError(writer, request, err)
		return
	}
	items := make([]map[string]any, 0, len(resp.GetPackets()))
	for _, packet := range resp.GetPackets() {
		items = append(items, voiceRoomPacketFromProto(packet))
	}
	httpkit.WriteOK(writer, request, map[string]any{
		"items":      items,
		"total":      resp.GetTotal(),
		"page":       page,
		"pageSize":   pageSize,
		"serverTime": resp.GetServerTimeMs(),
	})
}

func (h *Handler) getRedPacketConfig(writer http.ResponseWriter, request *http.Request) {
	if h.walletClient == nil {
		httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
		return
	}
	resp, err := h.walletClient.GetRedPacketConfig(request.Context(), &walletv1.GetRedPacketConfigRequest{
		AppCode: appcode.FromContext(request.Context()),
	})
	if err != nil {
		httpkit.WriteRPCError(writer, request, err)
		return
	}
	httpkit.WriteOK(writer, request, map[string]any{
		"config":         redPacketConfigFromProto(resp.GetConfig()),
		"server_time_ms": resp.GetServerTimeMs(),
	})
}

func (h *Handler) listRoomRedPackets(writer http.ResponseWriter, request *http.Request) {
	if 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
	}
	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
	}
	resp, err := h.walletClient.ListRedPackets(request.Context(), &walletv1.ListRedPacketsRequest{
		AppCode:      appcode.FromContext(request.Context()),
		RoomId:       roomID,
		PacketType:   strings.TrimSpace(request.URL.Query().Get("packet_type")),
		Status:       strings.TrimSpace(request.URL.Query().Get("status")),
		ViewerUserId: auth.UserIDFromContext(request.Context()),
		Page:         page,
		PageSize:     pageSize,
	})
	if err != nil {
		httpkit.WriteRPCError(writer, request, err)
		return
	}
	httpkit.WriteOK(writer, request, map[string]any{
		"packets":        redPacketsFromProto(resp.GetPackets()),
		"total":          resp.GetTotal(),
		"page":           page,
		"page_size":      pageSize,
		"server_time_ms": resp.GetServerTimeMs(),
	})
}

func (h *Handler) createRoomRedPacket(writer http.ResponseWriter, request *http.Request) {
	if h.walletClient == nil || 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
	}
	var body createRedPacketRequestBody
	if !httpkit.Decode(writer, request, &body) {
		return
	}
	commandID := body.normalizedCommandID()
	packetType := body.normalizedPacketType()
	totalAmount := body.normalizedTotalAmount()
	packetCount := body.normalizedPacketCount()
	if commandID == "" || packetType == "" || totalAmount <= 0 || packetCount <= 0 {
		httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
		return
	}
	snapshotResp, err := h.roomQueryClient.GetRoomSnapshot(request.Context(), &roomv1.GetRoomSnapshotRequest{
		Meta:         httpkit.RoomMeta(request, roomID, ""),
		RoomId:       roomID,
		ViewerUserId: auth.UserIDFromContext(request.Context()),
	})
	if err != nil {
		httpkit.WriteRPCError(writer, request, err)
		return
	}
	regionID := snapshotResp.GetRoom().GetVisibleRegionId()
	if regionID <= 0 {
		httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
		return
	}
	resp, err := h.walletClient.CreateRedPacket(request.Context(), &walletv1.CreateRedPacketRequest{
		AppCode:      appcode.FromContext(request.Context()),
		CommandId:    commandID,
		SenderUserId: auth.UserIDFromContext(request.Context()),
		RoomId:       roomID,
		RegionId:     regionID,
		PacketType:   packetType,
		TotalAmount:  totalAmount,
		PacketCount:  packetCount,
		RoomLocked:   snapshotResp.GetRoom().GetLocked(),
		OwnerUserId:  snapshotResp.GetRoom().GetOwnerUserId(),
	})
	if err != nil {
		httpkit.WriteRPCError(writer, request, err)
		return
	}
	httpkit.WriteOK(writer, request, map[string]any{
		"packet":         redPacketFromProto(resp.GetPacket()),
		"balance_after":  resp.GetBalanceAfter(),
		"server_time_ms": resp.GetServerTimeMs(),
	})
}

func (h *Handler) getRedPacket(writer http.ResponseWriter, request *http.Request) {
	if h.walletClient == nil {
		httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
		return
	}
	packetID := strings.TrimSpace(request.PathValue("packet_id"))
	if packetID == "" {
		httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
		return
	}
	resp, err := h.walletClient.GetRedPacket(request.Context(), &walletv1.GetRedPacketRequest{
		AppCode:       appcode.FromContext(request.Context()),
		PacketId:      packetID,
		ViewerUserId:  auth.UserIDFromContext(request.Context()),
		IncludeClaims: boolQuery(request, "include_claims"),
	})
	if err != nil {
		httpkit.WriteRPCError(writer, request, err)
		return
	}
	httpkit.WriteOK(writer, request, map[string]any{
		"packet":         redPacketFromProto(resp.GetPacket()),
		"server_time_ms": resp.GetServerTimeMs(),
	})
}

func (h *Handler) claimRedPacket(writer http.ResponseWriter, request *http.Request) {
	if h.walletClient == nil {
		httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
		return
	}
	packetID := strings.TrimSpace(request.PathValue("packet_id"))
	if packetID == "" {
		httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
		return
	}
	var body claimRedPacketRequestBody
	if !httpkit.Decode(writer, request, &body) {
		return
	}
	commandID := body.normalizedCommandID()
	if commandID == "" {
		httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
		return
	}
	resp, err := h.walletClient.ClaimRedPacket(request.Context(), &walletv1.ClaimRedPacketRequest{
		AppCode:   appcode.FromContext(request.Context()),
		CommandId: commandID,
		PacketId:  packetID,
		UserId:    auth.UserIDFromContext(request.Context()),
	})
	if err != nil {
		httpkit.WriteRPCError(writer, request, err)
		return
	}
	httpkit.WriteOK(writer, request, map[string]any{
		"claim":          redPacketClaimFromProto(resp.GetClaim()),
		"balance_after":  resp.GetBalanceAfter(),
		"server_time_ms": resp.GetServerTimeMs(),
	})
}

func redPacketConfigFromProto(config *walletv1.RedPacketConfig) redPacketConfigData {
	if config == nil {
		return redPacketConfigData{}
	}
	return redPacketConfigData{
		AppCode:            config.GetAppCode(),
		Enabled:            config.GetEnabled(),
		CountTiers:         append([]int32(nil), config.GetCountTiers()...),
		AmountTiers:        append([]int64(nil), config.GetAmountTiers()...),
		DelayedOpenSeconds: config.GetDelayedOpenSeconds(),
		ExpireSeconds:      config.GetExpireSeconds(),
		DailySendLimit:     config.GetDailySendLimit(),
		UpdatedByAdminID:   config.GetUpdatedByAdminId(),
		RuleURL:            config.GetRuleUrl(),
		CreatedAtMS:        config.GetCreatedAtMs(),
		UpdatedAtMS:        config.GetUpdatedAtMs(),
	}
}

func redPacketsFromProto(items []*walletv1.RedPacket) []redPacketData {
	out := make([]redPacketData, 0, len(items))
	for _, item := range items {
		out = append(out, redPacketFromProto(item))
	}
	return out
}

func redPacketFromProto(packet *walletv1.RedPacket) redPacketData {
	if packet == nil {
		return redPacketData{}
	}
	claims := make([]redPacketClaimData, 0, len(packet.GetClaims()))
	for _, claim := range packet.GetClaims() {
		claims = append(claims, redPacketClaimFromProto(claim))
	}
	return redPacketData{
		AppCode:           packet.GetAppCode(),
		PacketID:          packet.GetPacketId(),
		CommandID:         packet.GetCommandId(),
		SenderUserID:      packet.GetSenderUserId(),
		RoomID:            packet.GetRoomId(),
		RegionID:          packet.GetRegionId(),
		PacketType:        packet.GetPacketType(),
		TotalAmount:       packet.GetTotalAmount(),
		PacketCount:       packet.GetPacketCount(),
		RemainingAmount:   packet.GetRemainingAmount(),
		RemainingCount:    packet.GetRemainingCount(),
		Status:            packet.GetStatus(),
		OpenAtMS:          packet.GetOpenAtMs(),
		ExpiresAtMS:       packet.GetExpiresAtMs(),
		CreatedAtMS:       packet.GetCreatedAtMs(),
		UpdatedAtMS:       packet.GetUpdatedAtMs(),
		ClaimedByViewer:   packet.GetClaimedByViewer(),
		ViewerClaimAmount: packet.GetViewerClaimAmount(),
		Claims:            claims,
	}
}

func redPacketClaimFromProto(claim *walletv1.RedPacketClaim) redPacketClaimData {
	if claim == nil {
		return redPacketClaimData{}
	}
	return redPacketClaimData{
		AppCode:             claim.GetAppCode(),
		ClaimID:             claim.GetClaimId(),
		CommandID:           claim.GetCommandId(),
		PacketID:            claim.GetPacketId(),
		UserID:              claim.GetUserId(),
		Amount:              claim.GetAmount(),
		WalletTransactionID: claim.GetWalletTransactionId(),
		Status:              claim.GetStatus(),
		FailureReason:       claim.GetFailureReason(),
		CreatedAtMS:         claim.GetCreatedAtMs(),
		UpdatedAtMS:         claim.GetUpdatedAtMs(),
	}
}

func boolQuery(request *http.Request, name string) bool {
	value := strings.TrimSpace(request.URL.Query().Get(name))
	if value == "" {
		return false
	}
	parsed, err := strconv.ParseBool(value)
	return err == nil && parsed
}

func (h *Handler) currentUser(writer http.ResponseWriter, request *http.Request) (*userv1.User, bool) {
	if h.userProfileClient == nil {
		httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
		return nil, false
	}
	resp, err := h.userProfileClient.GetUser(request.Context(), &userv1.GetUserRequest{
		Meta:   httpkit.UserMeta(request, ""),
		UserId: auth.UserIDFromContext(request.Context()),
	})
	if err != nil {
		httpkit.WriteRPCError(writer, request, err)
		return nil, false
	}
	user := resp.GetUser()
	if user == nil {
		httpkit.WriteError(writer, request, http.StatusInternalServerError, httpkit.CodeInternalError, "internal error")
		return nil, false
	}
	return user, true
}

func (h *Handler) verifyRedPacketRoomPresence(writer http.ResponseWriter, request *http.Request, roomID string) bool {
	if !roomid.ValidStringID(roomID) {
		httpkit.WriteError(writer, request, http.StatusConflict, "voice_room_red_packet_room_presence_required", "join the room before using voice room red packet")
		return false
	}
	resp, err := h.roomGuardClient.VerifyRoomPresence(request.Context(), &roomv1.VerifyRoomPresenceRequest{
		RoomId:    roomID,
		UserId:    auth.UserIDFromContext(request.Context()),
		RequestId: httpkit.RequestIDFromContext(request.Context()),
		AppCode:   appcode.FromContext(request.Context()),
	})
	if err != nil {
		httpkit.WriteRPCError(writer, request, err)
		return false
	}
	if !resp.GetPresent() {
		httpkit.WriteError(writer, request, http.StatusConflict, "voice_room_red_packet_room_presence_required", "join the room before using voice room red packet")
		return false
	}
	return true
}

func writeRedPacketRPCError(writer http.ResponseWriter, request *http.Request, err error) {
	if err == nil {
		return
	}
	reason := xerr.ReasonFromGRPC(err)
	statusCode, code, message := httpkit.MapReasonToHTTP(reason)
	switch reason {
	case xerr.RedPacketDisabled:
		code = "voice_room_red_packet_disabled"
	case xerr.RedPacketInvalidAmountTier, xerr.RedPacketAmountTooSmall:
		code = "voice_room_red_packet_amount_invalid"
	case xerr.RedPacketInvalidCountTier:
		code = "voice_room_red_packet_count_invalid"
	case xerr.RedPacketDailyLimitReached:
		code = "voice_room_red_packet_daily_limit_exceeded"
	case xerr.InsufficientBalance:
		code = "voice_room_red_packet_insufficient_balance"
	case xerr.NotFound:
		code = "voice_room_red_packet_not_found"
	case xerr.RedPacketNotOpen:
		code = "voice_room_red_packet_not_started"
	case xerr.RedPacketExpired:
		code = "voice_room_red_packet_expired"
	case xerr.RedPacketSoldOut:
		code = "voice_room_red_packet_finished"
	case xerr.LedgerConflict:
		code = "claim_processing"
	}
	httpkit.WriteError(writer, request, statusCode, code, message)
}

func internalRedPacketType(mode string) string {
	switch strings.ToUpper(strings.TrimSpace(mode)) {
	case "IMMEDIATE":
		return "normal"
	case "DELAYED":
		return "delayed"
	}
	switch strings.ToLower(strings.TrimSpace(mode)) {
	case "normal":
		return "normal"
	case "delayed":
		return "delayed"
	default:
		return ""
	}
}

func publicRedPacketMode(packetType string) string {
	switch strings.ToLower(strings.TrimSpace(packetType)) {
	case "normal":
		return "IMMEDIATE"
	case "delayed":
		return "DELAYED"
	default:
		return strings.ToUpper(strings.TrimSpace(packetType))
	}
}

func publicRedPacketStatus(status string) string {
	switch strings.ToLower(strings.TrimSpace(status)) {
	case "waiting_open":
		return "WAITING_OPEN"
	case "active":
		return "ACTIVE"
	case "finished":
		return "FINISHED"
	case "refunded":
		return "REFUNDED"
	default:
		return strings.ToUpper(strings.TrimSpace(status))
	}
}

func voiceRoomPacketFromProto(packet *walletv1.RedPacket) map[string]any {
	if packet == nil {
		return map[string]any{}
	}
	claims := make([]map[string]any, 0, len(packet.GetClaims()))
	for _, claim := range packet.GetClaims() {
		claims = append(claims, voiceRoomClaimFromProto(claim))
	}
	return map[string]any{
		"packetNo":        packet.GetPacketId(),
		"roomId":          packet.GetRoomId(),
		"regionId":        packet.GetRegionId(),
		"senderUserId":    packet.GetSenderUserId(),
		"packetMode":      publicRedPacketMode(packet.GetPacketType()),
		"totalAmount":     packet.GetTotalAmount(),
		"totalCount":      packet.GetPacketCount(),
		"remainingAmount": packet.GetRemainingAmount(),
		"remainingCount":  packet.GetRemainingCount(),
		"status":          publicRedPacketStatus(packet.GetStatus()),
		"claimStartTime":  packet.GetOpenAtMs(),
		"expireTime":      packet.GetExpiresAtMs(),
		"createdAt":       packet.GetCreatedAtMs(),
		"claimedByMe":     packet.GetClaimedByViewer(),
		"myClaimAmount":   packet.GetViewerClaimAmount(),
		"refundAmount":    packet.GetRefundAmount(),
		"refundStatus":    packet.GetRefundStatus(),
		"refundedAt":      packet.GetRefundedAtMs(),
		"claims":          claims,
	}
}

func voiceRoomPacketFromProtoWithSender(packet *walletv1.RedPacket, sender *userv1.User) map[string]any {
	data := voiceRoomPacketFromProto(packet)
	if packet == nil {
		return data
	}
	account := ""
	nickname := ""
	avatar := ""
	if sender != nil {
		account = sender.GetDisplayUserId()
		nickname = sender.GetUsername()
		avatar = sender.GetAvatar()
	}
	if account == "" && packet.GetSenderUserId() > 0 {
		account = strconv.FormatInt(packet.GetSenderUserId(), 10)
	}
	// wallet 只拥有红包和账务事实；HTTP 展示字段在 gateway 出口由 user-service 资料补齐。
	data["senderAccount"] = account
	data["senderNickname"] = nickname
	data["senderAvatar"] = avatar
	return data
}

func voiceRoomClaimFromProto(claim *walletv1.RedPacketClaim) map[string]any {
	return voiceRoomClaimFromProtoWithUser(claim, nil)
}

func voiceRoomClaimFromProtoWithUser(claim *walletv1.RedPacketClaim, user *userv1.User) map[string]any {
	if claim == nil {
		return map[string]any{}
	}
	account := ""
	nickname := ""
	avatar := ""
	if user != nil {
		account = user.GetDisplayUserId()
		nickname = user.GetUsername()
		avatar = user.GetAvatar()
	}
	if account == "" && claim.GetUserId() > 0 {
		account = strconv.FormatInt(claim.GetUserId(), 10)
	}
	return map[string]any{
		"claimId":   claim.GetClaimId(),
		"packetNo":  claim.GetPacketId(),
		"userId":    claim.GetUserId(),
		"account":   account,
		"nickname":  nickname,
		"avatar":    avatar,
		"amount":    claim.GetAmount(),
		"status":    claim.GetStatus(),
		"createdAt": claim.GetCreatedAtMs(),
	}
}
