package service

import (
	"context"
	"log/slog"
	"strings"
	"time"

	roomv1 "hyapp.local/api/proto/room/v1"
	"hyapp/pkg/appcode"
	"hyapp/pkg/logx"
	"hyapp/pkg/xerr"
	"hyapp/services/room-service/internal/room/state"
)

const (
	maxUserVoiceRoomPresenceBatchSize = 100
	userVoiceRoomStateOnMic           = "on_mic"
	userVoiceRoomStateNotOnMic        = "not_on_mic"
)

// BatchGetUserVoiceRoomPresences 返回个人资料页可以安全展示的实时上麦入口。
// room_user_presence 只负责把 user_id 定位到候选 Room Cell；上麦、房间生命周期和 viewer 权限最终都以最新快照为准。
func (s *Service) BatchGetUserVoiceRoomPresences(ctx context.Context, req *roomv1.BatchGetUserVoiceRoomPresencesRequest) (*roomv1.BatchGetUserVoiceRoomPresencesResponse, error) {
	startedAt := time.Now()
	if req == nil || req.GetViewerUserId() <= 0 {
		return nil, xerr.New(xerr.InvalidArgument, "viewer_user_id is required")
	}

	userIDs, ok := normalizedVoiceRoomPresenceUserIDs(req.GetUserIds())
	if !ok {
		return nil, xerr.New(xerr.InvalidArgument, "user_ids must contain 1 to 100 positive ids")
	}
	ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode())

	presences, err := s.repository.BatchGetCurrentRoomPresences(ctx, userIDs)
	if err != nil {
		return nil, err
	}

	// 一个 batch 可能包含同房间多个麦上用户；每个候选房间只恢复/读取一次快照，避免重复触发 Room Cell 或 MySQL 恢复链路。
	snapshots := make(map[string]*roomv1.RoomSnapshot)
	for userID, presence := range presences {
		if userID == req.GetViewerUserId() || strings.TrimSpace(presence.RoomID) == "" {
			continue
		}
		if _, loaded := snapshots[presence.RoomID]; loaded {
			continue
		}
		snapshot, err := s.currentSnapshot(ctx, presence.RoomID)
		if err != nil {
			return nil, err
		}
		snapshots[presence.RoomID] = snapshot
	}

	nowMS := s.clock.Now().UnixMilli()
	resolved := make(map[int64]*roomv1.UserVoiceRoomPresence, len(userIDs))
	onMicCount := 0
	hiddenCount := 0
	orphanCount := 0
	for _, userID := range userIDs {
		item := &roomv1.UserVoiceRoomPresence{
			UserId:       userID,
			State:        userVoiceRoomStateNotOnMic,
			ObservedAtMs: nowMS,
		}
		resolved[userID] = item

		// 当前产品只在客态资料页展示 Live；查询本人时按不可见结果收敛，不泄露多余房间入口。
		if userID == req.GetViewerUserId() {
			hiddenCount++
			continue
		}
		presence, exists := presences[userID]
		if !exists {
			continue
		}
		snapshot := snapshots[presence.RoomID]
		if snapshot == nil || snapshot.GetStatus() != state.RoomStatusActive || snapshot.GetAppCode() != appcode.FromContext(ctx) {
			// presence 投影允许短暂滞后；孤儿索引只能降级为 not_on_mic，不能返回旧 room_id。
			orphanCount++
			continue
		}
		if snapshotUserBanned(snapshot, req.GetViewerUserId(), nowMS) {
			// viewer 被房间封禁时正式进房必然失败，因此这里隐藏全部房间定位字段。
			hiddenCount++
			continue
		}
		seat := protoSeatByUser(snapshot, userID)
		if seat == nil {
			// 仅在房间内但没有占用麦位不是上麦状态。
			continue
		}
		if findProtoUser(snapshot, userID) == nil {
			// 麦位用户不在当前 online_users 说明快照内部事实不完整，按异常数据隐藏并留日志计数。
			orphanCount++
			continue
		}

		ext := snapshot.GetRoomExt()
		roomShortID := snapshot.GetRoomShortId()
		if roomShortID == "" {
			roomShortID = ext["room_short_id"]
		}
		onlineCount := snapshot.GetOnlineCount()
		if onlineCount <= 0 && len(snapshot.GetOnlineUsers()) > 0 {
			onlineCount = int32(len(snapshot.GetOnlineUsers()))
		}
		item.State = userVoiceRoomStateOnMic
		item.SeatNo = seat.GetSeatNo()
		item.Joinable = true
		item.Room = &roomv1.UserVoiceRoomPresenceRoom{
			RoomId:        snapshot.GetRoomId(),
			RoomShortId:   roomShortID,
			OwnerUserId:   snapshot.GetOwnerUserId(),
			HostUserId:    snapshot.GetOwnerUserId(),
			Title:         ext["title"],
			CoverUrl:      ext["cover_url"],
			BackgroundUrl: ext["background_url"],
			Mode:          snapshot.GetMode(),
			OnlineCount:   onlineCount,
			SeatCount:     int32(len(snapshot.GetMicSeats())),
			Locked:        snapshot.GetLocked(),
		}
		onMicCount++
	}

	items := make([]*roomv1.UserVoiceRoomPresence, 0, len(userIDs))
	for _, userID := range userIDs {
		items = append(items, resolved[userID])
	}
	logx.Info(ctx, "user_voice_room_presence_batch_queried",
		slog.String("request_id", req.GetMeta().GetRequestId()),
		slog.String("app_code", appcode.FromContext(ctx)),
		slog.Int64("viewer_user_id", req.GetViewerUserId()),
		slog.Int("user_count", len(userIDs)),
		slog.Int("active_presence_count", len(presences)),
		slog.Int("on_mic_count", onMicCount),
		slog.Int("hidden_count", hiddenCount),
		slog.Int("orphan_count", orphanCount),
		slog.Int64("duration_ms", time.Since(startedAt).Milliseconds()),
	)
	return &roomv1.BatchGetUserVoiceRoomPresencesResponse{Items: items, ServerTimeMs: nowMS}, nil
}

func normalizedVoiceRoomPresenceUserIDs(values []int64) ([]int64, bool) {
	if len(values) == 0 {
		return nil, false
	}
	seen := make(map[int64]struct{}, len(values))
	result := make([]int64, 0, len(values))
	for _, userID := range values {
		if userID <= 0 {
			return nil, false
		}
		if _, exists := seen[userID]; exists {
			continue
		}
		seen[userID] = struct{}{}
		result = append(result, userID)
		if len(result) > maxUserVoiceRoomPresenceBatchSize {
			return nil, false
		}
	}
	return result, len(result) > 0
}
