package service

import (
	"context"
	"encoding/json"
	"fmt"
	"sort"
	"strconv"
	"strings"

	"github.com/redis/go-redis/v9"
	"hyapp/pkg/appcode"
)

const (
	// v2 表示独立 namespace 已同时重建区域桶和全区桶；ready 之前一律回退 MySQL。
	roomListCacheReadyValue      = "2"
	roomListCacheCountryAll      = "_"
	roomListCacheAllRegionsScope = "all"
	roomListCacheBucketOnline    = "online"
	roomListCacheBucketEmpty     = "empty"
	roomListCacheDefaultScanSize = 128
	roomListCacheMaxScanSize     = 1024
	roomListCacheMaxInt64        = int64(^uint64(0) >> 1)
)

// RedisRoomListCacheStore 用 Redis zset/hash 承载发现页 hot/new 展示读模型。
//
// 该实现只保存可重建的列表投影，不参与 Room Cell 权威状态、command log 或 MySQL owner 表语义。
// 一旦缓存未 ready、扫描保护触发或 Redis 报错，Service 会回退到 MySQL 复杂 SQL，保证功能正确性优先。
type RedisRoomListCacheStore struct {
	client *redis.Client
}

type roomListCacheMemberRef struct {
	Key    string `json:"key"`
	Member string `json:"member"`
}

type roomListCachePinRow struct {
	Pin    RoomListPinEntry
	Member string
}

type roomListCacheCandidate struct {
	Entry     RoomListEntry
	SortValue int64
	// PinType 只用于同一房间命中多种置顶时复刻 MySQL CASE 的 global > region > country 选择优先级。
	PinType string
}

// NewRedisRoomListCacheStore 创建公共发现页 Redis 展示读模型。
func NewRedisRoomListCacheStore(client *redis.Client) *RedisRoomListCacheStore {
	return &RedisRoomListCacheStore{client: client}
}

// ListRoomListEntries 从 Redis 读取一页公共发现列表；ok=false 表示调用方必须回退 MySQL。
func (s *RedisRoomListCacheStore) ListRoomListEntries(ctx context.Context, query RoomListQuery) ([]RoomListEntry, bool, error) {
	if s == nil || s.client == nil || !roomListCacheEligible(query) {
		return nil, false, nil
	}
	query.AppCode = appcode.Normalize(query.AppCode)
	if query.AppCode == "" {
		query.AppCode = appcode.Default
	}
	if query.Limit <= 0 {
		query.Limit = defaultRoomListLimit
	}
	if query.NowMS <= 0 {
		return nil, false, nil
	}
	ready, err := s.client.Get(ctx, roomListCacheReadyKey(query.AppCode)).Result()
	if err == redis.Nil {
		return nil, false, nil
	}
	if err != nil {
		return nil, false, err
	}
	if ready != roomListCacheReadyValue {
		return nil, false, nil
	}

	scanSize := roomListCacheScanSize(query.Limit)
	candidates := make(map[string]roomListCacheCandidate, scanSize)
	truncated := false

	pinRows, pinTruncated, err := s.listRoomListCachePins(ctx, query, scanSize)
	if err != nil {
		return nil, false, err
	}
	truncated = truncated || pinTruncated
	pinRoomIDs := make([]string, 0, len(pinRows))
	for _, row := range pinRows {
		pinRoomIDs = append(pinRoomIDs, row.Pin.RoomID)
	}
	pinCards, err := s.roomListCacheCards(ctx, query.AppCode, pinRoomIDs)
	if err != nil {
		return nil, false, err
	}
	for _, row := range pinRows {
		card, ok := pinCards[row.Pin.RoomID]
		if !ok || !roomListCacheCardMatches(query, card) || !roomListCachePinApplies(query, row.Pin, card) {
			continue
		}
		rank := int64(0)
		if row.Pin.PinType == roomPinTypeCountry {
			rank = 1
		}
		card.IsPinned = true
		card.PinListRank = rank
		card.PinWeight = row.Pin.Weight
		card.PinnedUntilMS = row.Pin.ExpiresAtMS
		roomListCacheKeepBest(candidates, roomListCacheCandidate{Entry: card, SortValue: roomListCacheSortValue(query.Tab, card), PinType: row.Pin.PinType})
	}

	normalIDs, normalTruncated, err := s.listRoomListCacheNormalRoomIDs(ctx, query, scanSize)
	if err != nil {
		return nil, false, err
	}
	truncated = truncated || normalTruncated
	normalCards, err := s.roomListCacheCards(ctx, query.AppCode, normalIDs)
	if err != nil {
		return nil, false, err
	}
	for _, roomID := range normalIDs {
		card, ok := normalCards[roomID]
		if !ok || !roomListCacheCardMatches(query, card) {
			continue
		}
		card.IsPinned = false
		card.PinListRank = roomListCacheNormalRank(query, card)
		card.PinWeight = 0
		card.PinnedUntilMS = 0
		roomListCacheKeepBest(candidates, roomListCacheCandidate{Entry: card, SortValue: roomListCacheSortValue(query.Tab, card)})
	}

	ordered := make([]roomListCacheCandidate, 0, len(candidates))
	for _, candidate := range candidates {
		ordered = append(ordered, candidate)
	}
	sort.SliceStable(ordered, func(i, j int) bool {
		return roomListCacheCandidateBefore(ordered[i], ordered[j])
	})

	out := make([]RoomListEntry, 0, query.Limit)
	for _, candidate := range ordered {
		if !roomListCacheAfterCursor(query, candidate.Entry, candidate.SortValue) {
			continue
		}
		out = append(out, candidate.Entry)
		if len(out) >= query.Limit {
			return out, true, nil
		}
	}
	if truncated {
		// 当前 Redis 查询只扫描每个桶的前 N 个 member；深分页或极端同桶数据量交给 MySQL 保证正确性。
		return nil, false, nil
	}
	return out, true, nil
}

// UpsertRoomListEntry 刷新单个房间卡片和它所在的 hot/new zset 桶。
func (s *RedisRoomListCacheStore) UpsertRoomListEntry(ctx context.Context, entry RoomListEntry) error {
	if s == nil || s.client == nil {
		return nil
	}
	app := appcode.Normalize(entry.AppCode)
	if app == "" {
		app = appcode.Default
	}
	roomID := strings.TrimSpace(entry.RoomID)
	if roomID == "" {
		return nil
	}
	if strings.TrimSpace(entry.Status) != "active" {
		return s.DeleteRoomListEntry(ctx, app, roomID)
	}

	if err := s.removeRoomListEntryIndexes(ctx, app, roomID); err != nil {
		return err
	}
	entry.AppCode = app
	entry.OwnerCountryCode = normalizeRoomCountryCode(entry.OwnerCountryCode)
	if !entry.OwnerVIPProjectionKnown || !entry.ActiveRedPacketProjectionKnown {
		// Room Cell 的普通状态刷新不携带 wallet 外部事实。只读一次旧卡片并按 known 标记合并，避免
		// 送礼/上下麦/火箭进度刷新误清 VIP 或红包，同时不为一次投影写入制造重复 Redis 往返。
		raw, err := s.client.HGet(ctx, roomListCacheCardKey(app), roomID).Result()
		if err != nil && err != redis.Nil {
			return err
		}
		if err == nil {
			var previous RoomListEntry
			if json.Unmarshal([]byte(raw), &previous) == nil {
				if !entry.OwnerVIPProjectionKnown {
					entry.OwnerVIPLevel = previous.OwnerVIPLevel
					entry.OwnerVIPExpiresAtMS = previous.OwnerVIPExpiresAtMS
					entry.OwnerVIPFallbackLevel = previous.OwnerVIPFallbackLevel
					entry.OwnerVIPFallbackExpiresAtMS = previous.OwnerVIPFallbackExpiresAtMS
				}
				if !entry.ActiveRedPacketProjectionKnown {
					entry.HasActiveRedPacket = previous.HasActiveRedPacket
					entry.ActiveRedPacketExpiresAtMS = previous.ActiveRedPacketExpiresAtMS
				}
			}
		}
	}
	payload, err := json.Marshal(entry)
	if err != nil {
		return err
	}
	refs := roomListCacheEntryRefs(app, entry)
	rawRefs, err := json.Marshal(refs)
	if err != nil {
		return err
	}

	pipe := s.client.Pipeline()
	pipe.HSet(ctx, roomListCacheCardKey(app), roomID, payload)
	pipe.HSet(ctx, roomListCacheEntryIndexKey(app), roomID, rawRefs)
	for _, ref := range refs {
		pipe.SAdd(ctx, roomListCacheKnownKeysKey(app), ref.Key)
		pipe.ZAdd(ctx, ref.Key, redis.Z{Score: roomListCacheEntryScore(ref.Key, entry), Member: ref.Member})
	}
	_, err = pipe.Exec(ctx)
	return err
}

// DeleteRoomListEntry 删除单个房间缓存投影。
func (s *RedisRoomListCacheStore) DeleteRoomListEntry(ctx context.Context, appCode string, roomID string) error {
	if s == nil || s.client == nil {
		return nil
	}
	app := appcode.Normalize(appCode)
	if app == "" {
		app = appcode.Default
	}
	roomID = strings.TrimSpace(roomID)
	if roomID == "" {
		return nil
	}
	if err := s.removeRoomListEntryIndexes(ctx, app, roomID); err != nil {
		return err
	}
	_, err := s.client.Pipelined(ctx, func(pipe redis.Pipeliner) error {
		pipe.HDel(ctx, roomListCacheCardKey(app), roomID)
		pipe.HDel(ctx, roomListCacheEntryIndexKey(app), roomID)
		return nil
	})
	return err
}

// UpsertRoomPin 刷新单条置顶缓存；非 active、已过期或字段无效时移除旧索引。
func (s *RedisRoomListCacheStore) UpsertRoomPin(ctx context.Context, pin RoomListPinEntry) error {
	if s == nil || s.client == nil {
		return nil
	}
	app := appcode.Normalize(pin.AppCode)
	if app == "" {
		app = appcode.Default
	}
	pin.AppCode = app
	if pin.ID <= 0 || strings.TrimSpace(pin.RoomID) == "" {
		return nil
	}
	if err := s.removeRoomListPinIndexes(ctx, app, pin.ID); err != nil {
		return err
	}
	pin.PinType = roomListCacheNormalizePinType(pin.PinType)
	if strings.TrimSpace(pin.Status) != "active" || pin.PinType == "" || pin.ExpiresAtMS <= pin.PinnedAtMS {
		return nil
	}

	payload, err := json.Marshal(pin)
	if err != nil {
		return err
	}
	refs := roomListCachePinRefs(app, pin)
	rawRefs, err := json.Marshal(refs)
	if err != nil {
		return err
	}
	pipe := s.client.Pipeline()
	pipe.HSet(ctx, roomListCachePinPayloadKey(app), strconv.FormatInt(pin.ID, 10), payload)
	pipe.HSet(ctx, roomListCachePinIndexKey(app), strconv.FormatInt(pin.ID, 10), rawRefs)
	for _, ref := range refs {
		pipe.SAdd(ctx, roomListCacheKnownKeysKey(app), ref.Key)
		pipe.ZAdd(ctx, ref.Key, redis.Z{Score: -float64(pin.Weight), Member: ref.Member})
	}
	_, err = pipe.Exec(ctx)
	return err
}

// RebuildRoomList 清空某个 App 的旧缓存后，用 MySQL 扫描结果重建并标记 ready。
func (s *RedisRoomListCacheStore) RebuildRoomList(ctx context.Context, appCode string, entries []RoomListEntry, pins []RoomListPinEntry, _ int64) error {
	if s == nil || s.client == nil {
		return nil
	}
	app := appcode.Normalize(appCode)
	if app == "" {
		app = appcode.Default
	}
	knownKeys, err := s.client.SMembers(ctx, roomListCacheKnownKeysKey(app)).Result()
	if err != nil {
		return err
	}
	deleteKeys := append([]string{}, knownKeys...)
	deleteKeys = append(deleteKeys,
		roomListCacheCardKey(app),
		roomListCacheEntryIndexKey(app),
		roomListCachePinPayloadKey(app),
		roomListCachePinIndexKey(app),
		roomListCacheReadyKey(app),
		roomListCacheKnownKeysKey(app),
	)
	if len(deleteKeys) > 0 {
		if err := s.client.Del(ctx, deleteKeys...).Err(); err != nil {
			return err
		}
	}
	for _, entry := range entries {
		if entry.AppCode == app {
			if err := s.UpsertRoomListEntry(ctx, entry); err != nil {
				return err
			}
		}
	}
	for _, pin := range pins {
		if appcode.Normalize(pin.AppCode) == app {
			if err := s.UpsertRoomPin(ctx, pin); err != nil {
				return err
			}
		}
	}
	return s.client.Set(ctx, roomListCacheReadyKey(app), roomListCacheReadyValue, 0).Err()
}

func (s *RedisRoomListCacheStore) removeRoomListEntryIndexes(ctx context.Context, app string, roomID string) error {
	raw, err := s.client.HGet(ctx, roomListCacheEntryIndexKey(app), roomID).Result()
	if err == redis.Nil {
		return nil
	}
	if err != nil {
		return err
	}
	var refs []roomListCacheMemberRef
	if err := json.Unmarshal([]byte(raw), &refs); err != nil {
		return err
	}
	if len(refs) == 0 {
		return nil
	}
	pipe := s.client.Pipeline()
	for _, ref := range refs {
		if strings.TrimSpace(ref.Key) != "" && strings.TrimSpace(ref.Member) != "" {
			pipe.ZRem(ctx, ref.Key, ref.Member)
		}
	}
	_, err = pipe.Exec(ctx)
	return err
}

func (s *RedisRoomListCacheStore) removeRoomListPinIndexes(ctx context.Context, app string, pinID int64) error {
	field := strconv.FormatInt(pinID, 10)
	raw, err := s.client.HGet(ctx, roomListCachePinIndexKey(app), field).Result()
	if err == redis.Nil {
		return nil
	}
	if err != nil {
		return err
	}
	var refs []roomListCacheMemberRef
	if err := json.Unmarshal([]byte(raw), &refs); err != nil {
		return err
	}
	pipe := s.client.Pipeline()
	for _, ref := range refs {
		if strings.TrimSpace(ref.Key) != "" && strings.TrimSpace(ref.Member) != "" {
			pipe.ZRem(ctx, ref.Key, ref.Member)
		}
	}
	pipe.HDel(ctx, roomListCachePinIndexKey(app), field)
	pipe.HDel(ctx, roomListCachePinPayloadKey(app), field)
	_, err = pipe.Exec(ctx)
	return err
}

func (s *RedisRoomListCacheStore) listRoomListCachePins(ctx context.Context, query RoomListQuery, scanSize int) ([]roomListCachePinRow, bool, error) {
	keys := []string{roomListCachePinKey(query.AppCode, roomPinTypeGlobal, 0)}
	if query.AllVisibleRegions {
		// All 列表不能复用 region=0；0 是真实 GLOBAL 桶，独立 all key 才能合并每个区域的置顶事实。
		keys = append(keys, roomListCacheAllRegionsPinKey(query.AppCode, roomPinTypeRegion))
		if roomListCacheSortCountry(query) != "" {
			keys = append(keys, roomListCacheAllRegionsPinKey(query.AppCode, roomPinTypeCountry))
		}
	} else {
		keys = append(keys, roomListCachePinKey(query.AppCode, roomPinTypeRegion, normalizeVisibleRegionID(query.VisibleRegionID)))
		if roomListCacheSortCountry(query) != "" {
			keys = append(keys, roomListCachePinKey(query.AppCode, roomPinTypeCountry, normalizeVisibleRegionID(query.VisibleRegionID)))
		}
	}

	pinIDs := make([]string, 0, len(keys)*scanSize)
	membersByID := make(map[string]string, len(keys)*scanSize)
	truncated := false
	for _, key := range keys {
		rows, err := s.client.ZRangeWithScores(ctx, key, 0, int64(scanSize-1)).Result()
		if err != nil {
			return nil, false, err
		}
		total, err := s.client.ZCard(ctx, key).Result()
		if err != nil {
			return nil, false, err
		}
		truncated = truncated || total > int64(len(rows))
		for _, row := range rows {
			member, ok := row.Member.(string)
			if !ok {
				continue
			}
			pinID := roomListCachePinIDFromMember(member)
			if pinID == "" {
				continue
			}
			pinIDs = append(pinIDs, pinID)
			membersByID[pinID] = member
		}
	}
	if len(pinIDs) == 0 {
		return nil, truncated, nil
	}
	rawPins, err := s.client.HMGet(ctx, roomListCachePinPayloadKey(query.AppCode), pinIDs...).Result()
	if err != nil {
		return nil, false, err
	}
	out := make([]roomListCachePinRow, 0, len(rawPins))
	for index, raw := range rawPins {
		text, ok := raw.(string)
		if !ok || strings.TrimSpace(text) == "" {
			continue
		}
		var pin RoomListPinEntry
		if err := json.Unmarshal([]byte(text), &pin); err != nil {
			return nil, false, err
		}
		if pin.Status != "active" || pin.PinnedAtMS > query.NowMS || pin.ExpiresAtMS <= query.NowMS {
			continue
		}
		pinID := pinIDs[index]
		out = append(out, roomListCachePinRow{Pin: pin, Member: membersByID[pinID]})
	}
	return out, truncated, nil
}

func (s *RedisRoomListCacheStore) listRoomListCacheNormalRoomIDs(ctx context.Context, query RoomListQuery, scanSize int) ([]string, bool, error) {
	keys := roomListCacheNormalKeys(query)
	seen := make(map[string]struct{}, len(keys)*scanSize)
	out := make([]string, 0, len(keys)*scanSize)
	truncated := false
	for _, key := range keys {
		rows, err := s.client.ZRange(ctx, key, 0, int64(scanSize-1)).Result()
		if err != nil {
			return nil, false, err
		}
		total, err := s.client.ZCard(ctx, key).Result()
		if err != nil {
			return nil, false, err
		}
		truncated = truncated || total > int64(len(rows))
		for _, roomID := range rows {
			roomID = strings.TrimSpace(roomID)
			if roomID == "" {
				continue
			}
			if _, ok := seen[roomID]; ok {
				continue
			}
			seen[roomID] = struct{}{}
			out = append(out, roomID)
		}
	}
	return out, truncated, nil
}

func (s *RedisRoomListCacheStore) roomListCacheCards(ctx context.Context, app string, roomIDs []string) (map[string]RoomListEntry, error) {
	roomIDs = uniqueRoomListCacheIDs(roomIDs)
	cards := make(map[string]RoomListEntry, len(roomIDs))
	if len(roomIDs) == 0 {
		return cards, nil
	}
	fields := make([]string, 0, len(roomIDs))
	for _, roomID := range roomIDs {
		fields = append(fields, roomID)
	}
	rawCards, err := s.client.HMGet(ctx, roomListCacheCardKey(app), fields...).Result()
	if err != nil {
		return nil, err
	}
	for index, raw := range rawCards {
		text, ok := raw.(string)
		if !ok || strings.TrimSpace(text) == "" {
			continue
		}
		var entry RoomListEntry
		if err := json.Unmarshal([]byte(text), &entry); err != nil {
			return nil, err
		}
		cards[fields[index]] = entry
	}
	return cards, nil
}

func uniqueRoomListCacheIDs(values []string) []string {
	seen := make(map[string]struct{}, len(values))
	out := make([]string, 0, len(values))
	for _, value := range values {
		value = strings.TrimSpace(value)
		if value == "" {
			continue
		}
		if _, ok := seen[value]; ok {
			continue
		}
		seen[value] = struct{}{}
		out = append(out, value)
	}
	return out
}

func roomListCacheKeepBest(candidates map[string]roomListCacheCandidate, candidate roomListCacheCandidate) {
	current, exists := candidates[candidate.Entry.RoomID]
	if !exists {
		candidates[candidate.Entry.RoomID] = candidate
		return
	}
	if candidate.Entry.IsPinned && current.Entry.IsPinned {
		candidatePrecedence := roomListCachePinTypePrecedence(candidate.PinType)
		currentPrecedence := roomListCachePinTypePrecedence(current.PinType)
		if candidatePrecedence != currentPrecedence {
			// MySQL 对同房间先选 global，其次 region，最后 country；不允许次级类型用更高 weight 反超。
			if candidatePrecedence < currentPrecedence {
				candidates[candidate.Entry.RoomID] = candidate
			}
			return
		}
	}
	if roomListCacheCandidateBefore(candidate, current) {
		candidates[candidate.Entry.RoomID] = candidate
	}
}

func roomListCachePinTypePrecedence(pinType string) int {
	switch roomListCacheNormalizePinType(pinType) {
	case roomPinTypeGlobal:
		return 0
	case roomPinTypeRegion:
		return 1
	case roomPinTypeCountry:
		return 2
	default:
		return 3
	}
}

func roomListCacheCandidateBefore(left roomListCacheCandidate, right roomListCacheCandidate) bool {
	if left.Entry.PinListRank != right.Entry.PinListRank {
		return left.Entry.PinListRank < right.Entry.PinListRank
	}
	if left.Entry.PinWeight != right.Entry.PinWeight {
		return left.Entry.PinWeight > right.Entry.PinWeight
	}
	if left.Entry.PinnedUntilMS != right.Entry.PinnedUntilMS {
		return left.Entry.PinnedUntilMS > right.Entry.PinnedUntilMS
	}
	if left.SortValue != right.SortValue {
		return left.SortValue > right.SortValue
	}
	return left.Entry.RoomID < right.Entry.RoomID
}

func roomListCacheAfterCursor(query RoomListQuery, entry RoomListEntry, sortValue int64) bool {
	if strings.TrimSpace(query.CursorRoomID) == "" {
		return true
	}
	if entry.PinListRank != query.CursorPinListRank {
		return entry.PinListRank > query.CursorPinListRank
	}
	if entry.PinWeight != query.CursorPinWeight {
		return entry.PinWeight < query.CursorPinWeight
	}
	if entry.PinnedUntilMS != query.CursorPinnedUntilMS {
		return entry.PinnedUntilMS < query.CursorPinnedUntilMS
	}
	cursorSort := query.CursorSortScore
	if query.Tab == roomListTabNew {
		cursorSort = query.CursorCreatedAtMS
	}
	if sortValue != cursorSort {
		return sortValue < cursorSort
	}
	return entry.RoomID > query.CursorRoomID
}

func roomListCacheCardMatches(query RoomListQuery, entry RoomListEntry) bool {
	if appcode.Normalize(entry.AppCode) != appcode.Normalize(query.AppCode) || entry.Status != "active" {
		return false
	}
	if !query.AllVisibleRegions && normalizeVisibleRegionID(entry.VisibleRegionID) != normalizeVisibleRegionID(query.VisibleRegionID) {
		return false
	}
	if country := normalizeRoomCountryCode(query.CountryCode); country != "" && normalizeRoomCountryCode(entry.OwnerCountryCode) != country {
		return false
	}
	return true
}

func roomListCachePinApplies(query RoomListQuery, pin RoomListPinEntry, entry RoomListEntry) bool {
	switch pin.PinType {
	case roomPinTypeGlobal:
		return pin.VisibleRegionID == 0
	case roomPinTypeRegion:
		if query.AllVisibleRegions {
			return normalizeVisibleRegionID(pin.VisibleRegionID) == normalizeVisibleRegionID(entry.VisibleRegionID)
		}
		return normalizeVisibleRegionID(pin.VisibleRegionID) == normalizeVisibleRegionID(query.VisibleRegionID)
	case roomPinTypeCountry:
		pinRegionID := normalizeVisibleRegionID(query.VisibleRegionID)
		if query.AllVisibleRegions {
			pinRegionID = normalizeVisibleRegionID(entry.VisibleRegionID)
		}
		return normalizeVisibleRegionID(pin.VisibleRegionID) == pinRegionID && normalizeRoomCountryCode(entry.OwnerCountryCode) == roomListCacheSortCountry(query)
	default:
		return false
	}
}

func roomListCacheNormalRank(query RoomListQuery, entry RoomListEntry) int64 {
	sortCountry := roomListCacheSortCountry(query)
	if sortCountry == "" {
		if entry.OnlineCount > 0 {
			return 2
		}
		return 4
	}
	entryCountry := normalizeRoomCountryCode(entry.OwnerCountryCode)
	if entryCountry == sortCountry && entry.OnlineCount > 0 {
		return 2
	}
	if entryCountry != sortCountry && entry.OnlineCount > 0 {
		return 3
	}
	if entryCountry == sortCountry {
		return 4
	}
	return 5
}

func roomListCacheSortCountry(query RoomListQuery) string {
	if country := normalizeRoomCountryCode(query.CountryCode); country != "" {
		return country
	}
	return normalizeRoomCountryCode(query.ViewerCountryCode)
}

func roomListCacheSortValue(tab string, entry RoomListEntry) int64 {
	if tab == roomListTabNew {
		return entry.CreatedAtMS
	}
	return entry.SortScore
}

func roomListCacheScanSize(limit int) int {
	if limit <= 0 {
		return roomListCacheDefaultScanSize
	}
	scanSize := limit * 10
	if scanSize < roomListCacheDefaultScanSize {
		scanSize = roomListCacheDefaultScanSize
	}
	if scanSize > roomListCacheMaxScanSize {
		scanSize = roomListCacheMaxScanSize
	}
	return scanSize
}

func roomListCacheEntryRefs(app string, entry RoomListEntry) []roomListCacheMemberRef {
	bucket := roomListCacheBucketEmpty
	if entry.OnlineCount > 0 {
		bucket = roomListCacheBucketOnline
	}
	refs := make([]roomListCacheMemberRef, 0, 8)
	for _, tab := range []string{roomListTabHot, roomListTabNew} {
		refs = append(refs, roomListCacheMemberRef{
			Key:    roomListCacheBaseKey(app, tab, entry.VisibleRegionID, roomListCacheCountryAll, bucket),
			Member: entry.RoomID,
		}, roomListCacheMemberRef{
			Key:    roomListCacheAllRegionsBaseKey(app, tab, roomListCacheCountryAll, bucket),
			Member: entry.RoomID,
		})
		if country := normalizeRoomCountryCode(entry.OwnerCountryCode); country != "" {
			refs = append(refs, roomListCacheMemberRef{
				Key:    roomListCacheBaseKey(app, tab, entry.VisibleRegionID, country, bucket),
				Member: entry.RoomID,
			}, roomListCacheMemberRef{
				Key:    roomListCacheAllRegionsBaseKey(app, tab, country, bucket),
				Member: entry.RoomID,
			})
		}
	}
	return refs
}

func roomListCacheEntryScore(key string, entry RoomListEntry) float64 {
	if strings.Contains(key, ":new:") {
		return -float64(entry.CreatedAtMS)
	}
	return -float64(entry.SortScore)
}

func roomListCacheNormalKeys(query RoomListQuery) []string {
	app := appcode.Normalize(query.AppCode)
	regionID := normalizeVisibleRegionID(query.VisibleRegionID)
	sortCountry := roomListCacheSortCountry(query)
	key := func(country string, bucket string) string {
		if query.AllVisibleRegions {
			return roomListCacheAllRegionsBaseKey(app, query.Tab, country, bucket)
		}
		return roomListCacheBaseKey(app, query.Tab, regionID, country, bucket)
	}
	if filterCountry := normalizeRoomCountryCode(query.CountryCode); filterCountry != "" {
		return []string{
			key(filterCountry, roomListCacheBucketOnline),
			key(filterCountry, roomListCacheBucketEmpty),
		}
	}
	if sortCountry != "" {
		return []string{
			key(sortCountry, roomListCacheBucketOnline),
			key(roomListCacheCountryAll, roomListCacheBucketOnline),
			key(sortCountry, roomListCacheBucketEmpty),
			key(roomListCacheCountryAll, roomListCacheBucketEmpty),
		}
	}
	return []string{
		key(roomListCacheCountryAll, roomListCacheBucketOnline),
		key(roomListCacheCountryAll, roomListCacheBucketEmpty),
	}
}

func roomListCachePinRefs(app string, pin RoomListPinEntry) []roomListCacheMemberRef {
	refs := []roomListCacheMemberRef{{
		Key:    roomListCachePinKey(app, pin.PinType, pin.VisibleRegionID),
		Member: roomListCachePinMember(pin),
	}}
	if pin.PinType == roomPinTypeRegion || pin.PinType == roomPinTypeCountry {
		refs = append(refs, roomListCacheMemberRef{
			Key:    roomListCacheAllRegionsPinKey(app, pin.PinType),
			Member: roomListCachePinMember(pin),
		})
	}
	return refs
}

func roomListCachePinMember(pin RoomListPinEntry) string {
	// zset score 只表达 weight desc；member 前缀把 expires_at_ms desc 固定成 lex asc，便于 Redis 先取高价值候选。
	invertedExpires := roomListCacheMaxInt64 - pin.ExpiresAtMS
	return fmt.Sprintf("%019d|%020d|%s", invertedExpires, pin.ID, pin.RoomID)
}

func roomListCachePinIDFromMember(member string) string {
	parts := strings.SplitN(member, "|", 3)
	if len(parts) < 3 {
		return ""
	}
	id, err := strconv.ParseInt(parts[1], 10, 64)
	if err != nil || id <= 0 {
		return ""
	}
	return strconv.FormatInt(id, 10)
}

func roomListCacheNormalizePinType(pinType string) string {
	switch strings.ToLower(strings.TrimSpace(pinType)) {
	case roomPinTypeGlobal:
		return roomPinTypeGlobal
	case roomPinTypeRegion:
		return roomPinTypeRegion
	case roomPinTypeCountry:
		return roomPinTypeCountry
	default:
		return ""
	}
}

func roomListCachePrefix(app string) string {
	// v2 使用独立 namespace：滚动发布期间旧进程仍会按 v1 的 4-ref 索引删改，绝不能触碰新进程的 8-ref All 索引。
	return "room:list_cache:v2:" + appcode.Normalize(app)
}

func roomListCacheReadyKey(app string) string {
	return roomListCachePrefix(app) + ":ready"
}

func roomListCacheKnownKeysKey(app string) string {
	return roomListCachePrefix(app) + ":known_keys"
}

func roomListCacheCardKey(app string) string {
	return roomListCachePrefix(app) + ":cards"
}

func roomListCacheEntryIndexKey(app string) string {
	return roomListCachePrefix(app) + ":entry_indexes"
}

func roomListCachePinPayloadKey(app string) string {
	return roomListCachePrefix(app) + ":pin_payloads"
}

func roomListCachePinIndexKey(app string) string {
	return roomListCachePrefix(app) + ":pin_indexes"
}

func roomListCacheBaseKey(app string, tab string, regionID int64, country string, bucket string) string {
	country = normalizeRoomCountryCode(country)
	if country == "" {
		country = roomListCacheCountryAll
	}
	return fmt.Sprintf("%s:base:%s:%d:%s:%s", roomListCachePrefix(app), tab, normalizeVisibleRegionID(regionID), country, bucket)
}

func roomListCacheAllRegionsBaseKey(app string, tab string, country string, bucket string) string {
	country = normalizeRoomCountryCode(country)
	if country == "" {
		country = roomListCacheCountryAll
	}
	// all 使用文本 scope，不和业务上的 visible_region_id=0 GLOBAL 桶共用任何 key。
	return fmt.Sprintf("%s:base:%s:%s:%s:%s", roomListCachePrefix(app), roomListCacheAllRegionsScope, tab, country, bucket)
}

func roomListCachePinKey(app string, pinType string, regionID int64) string {
	if pinType == roomPinTypeGlobal {
		regionID = 0
	}
	return fmt.Sprintf("%s:pin:%s:%d", roomListCachePrefix(app), pinType, normalizeVisibleRegionID(regionID))
}

func roomListCacheAllRegionsPinKey(app string, pinType string) string {
	return fmt.Sprintf("%s:pin:%s:%s", roomListCachePrefix(app), roomListCacheAllRegionsScope, pinType)
}
