package service

import (
	"context"
	"fmt"
	"log/slog"
	"math/rand"
	"sort"
	"strings"
	"time"

	roomv1 "hyapp.local/api/proto/room/v1"
	"hyapp/pkg/appcode"
	"hyapp/pkg/logx"
	"hyapp/pkg/xerr"
)

const (
	humanRobotLuckyPoolID      = "human_robot_lucky_display"
	humanRobotSuperLuckyPoolID = "human_robot_super_lucky_display"

	defaultHumanRobotCandidateRoomMaxOnline  = int32(5)
	defaultHumanRobotRoomFullStopOnline      = int32(10)
	defaultHumanRobotRoomTargetMinOnline     = int32(8)
	defaultHumanRobotRoomTargetMaxOnline     = int32(10)
	defaultHumanRobotStayMinMS               = int64(60 * 1000)
	defaultHumanRobotStayMaxMS               = int64(10 * 60 * 1000)
	defaultHumanRobotReplaceMinMS            = int64(60 * 1000)
	defaultHumanRobotReplaceMaxMS            = int64(3 * 60 * 1000)
	defaultHumanRobotNormalGiftIntervalMS    = int64(10 * 1000)
	defaultHumanRobotNormalGiftIntervalMinMS = int64(1 * 1000)
	defaultHumanRobotNormalGiftIntervalMaxMS = int64(20 * 1000)
	defaultHumanRobotLuckyPauseMinMS         = int64(5 * 1000)
	defaultHumanRobotLuckyPauseMaxMS         = int64(20 * 1000)
	defaultHumanRobotLuckyComboMin           = int64(1)
	defaultHumanRobotLuckyComboMax           = int64(3)
	defaultHumanRobotMaxGiftSenders          = int64(1)
)

func (s *Service) AdminGetHumanRoomRobotConfig(ctx context.Context, req *roomv1.AdminGetHumanRoomRobotConfigRequest) (*roomv1.AdminGetHumanRoomRobotConfigResponse, error) {
	ctx = contextFromMeta(ctx, req.GetMeta())
	config, exists, err := s.repository.GetHumanRoomRobotConfig(ctx)
	if err != nil {
		return nil, err
	}
	if !exists {
		config = defaultHumanRoomRobotConfig(appcode.FromContext(ctx), s.clock.Now().UnixMilli())
	}
	return &roomv1.AdminGetHumanRoomRobotConfigResponse{Config: humanRoomRobotConfigToProto(config), ServerTimeMs: s.clock.Now().UnixMilli()}, nil
}

func (s *Service) AdminUpdateHumanRoomRobotConfig(ctx context.Context, req *roomv1.AdminUpdateHumanRoomRobotConfigRequest) (*roomv1.AdminUpdateHumanRoomRobotConfigResponse, error) {
	ctx = contextFromMeta(ctx, req.GetMeta())
	config, err := normalizeHumanRoomRobotConfig(appcode.FromContext(ctx), req.GetConfig(), req.GetAdminId(), s.clock.Now().UnixMilli())
	if err != nil {
		return nil, err
	}
	saved, err := s.repository.UpsertHumanRoomRobotConfig(ctx, config)
	if err != nil {
		return nil, err
	}
	if !saved.Enabled {
		s.stopAllHumanRoomRobotRuntimes()
	}
	return &roomv1.AdminUpdateHumanRoomRobotConfigResponse{Config: humanRoomRobotConfigToProto(saved), ServerTimeMs: s.clock.Now().UnixMilli()}, nil
}

// RunHumanRoomRobotRuntimeManager 定期把各国家空闲机器人补进低人数真人房间。
func (s *Service) RunHumanRoomRobotRuntimeManager(ctx context.Context, interval time.Duration) {
	if interval <= 0 {
		interval = 15 * time.Second
	}
	logx.Info(ctx, "human_room_robot_runtime_manager_started", slog.Int64("interval_ms", interval.Milliseconds()))
	ticker := time.NewTicker(interval)
	defer ticker.Stop()
	s.scanHumanRoomRobotRuntimes(ctx)
	for {
		select {
		case <-ctx.Done():
			s.stopAllHumanRoomRobotRuntimes()
			return
		case <-ticker.C:
			s.scanHumanRoomRobotRuntimes(ctx)
		}
	}
}

func (s *Service) scanHumanRoomRobotRuntimes(ctx context.Context) {
	config, exists, err := s.repository.GetHumanRoomRobotConfig(ctx)
	if err != nil {
		logx.Warn(ctx, "human_room_robot_config_load_failed", slog.String("error", err.Error()))
		return
	}
	if !exists {
		return
	}
	config = withHumanRoomRobotDefaults(config)
	if !config.Enabled {
		s.stopAllHumanRoomRobotRuntimes()
		return
	}
	countryLimits := humanRoomRobotCountryLimits(config)
	runtimeCounts := s.reconcileHumanRoomRobotRuntimeLimits(config, countryLimits)
	pools, err := s.loadHumanRoomRobotPools(ctx, config.AppCode)
	if err != nil {
		logx.Warn(ctx, "human_room_robot_pool_load_failed", slog.String("error", err.Error()))
		return
	}
	config.CountryPools = pools
	for _, pool := range config.CountryPools {
		countryCode := normalizeRoomCountryCode(pool.CountryCode)
		if countryCode == "" || len(pool.RobotUserIDs) == 0 {
			continue
		}
		if config.CountryLimitEnabled {
			maxRooms := countryLimits[countryCode]
			if maxRooms <= 0 || runtimeCounts[countryCode] >= maxRooms {
				continue
			}
		}
		// 国家行里的房主白名单只收窄当前国家的候选 SQL，不额外读取 Room Cell；旧的全局白名单只作为未配置国家行白名单时的兼容回退。
		rooms, err := s.repository.ListHumanRobotCandidateRooms(ctx, config.AppCode, countryCode, config.CandidateRoomMaxOnline, config.RoomTargetMaxOnline, 50, humanRoomRobotAllowedOwnersForCountry(config, countryCode))
		if err != nil {
			logx.Warn(ctx, "human_room_robot_candidate_rooms_failed", slog.String("country_code", countryCode), slog.String("error", err.Error()))
			continue
		}
		for _, room := range rooms {
			if config.CountryLimitEnabled && runtimeCounts[countryCode] >= countryLimits[countryCode] {
				break
			}
			if s.humanRoomRobotRuntimeConfigChanged(room.RoomID, config.UpdatedAtMS) {
				s.stopHumanRoomRobotRuntime(room.RoomID)
			}
			if s.humanRoomRobotRuntimeExists(room.RoomID) {
				continue
			}
			if s.startHumanRoomRobotRuntime(ctx, config, countryCode, room.RoomID, pool.RobotUserIDs) {
				runtimeCounts[countryCode]++
			}
		}
	}
}

func (s *Service) loadHumanRoomRobotPools(ctx context.Context, appCode string) ([]HumanRoomRobotCountryPool, error) {
	if s.humanRobotPoolProvider == nil {
		return nil, xerr.New(xerr.Unavailable, "human room robot pool provider is not configured")
	}
	items, err := s.humanRobotPoolProvider.ListHumanRoomRobotPools(ctx, appCode)
	if err != nil {
		return nil, err
	}
	pools := make([]HumanRoomRobotCountryPool, 0, len(items))
	for _, item := range items {
		countryCode := normalizeRoomCountryCode(item.CountryCode)
		ids := normalizeRobotUserIDs(item.RobotUserIDs)
		if countryCode == "" || len(ids) == 0 {
			continue
		}
		pools = append(pools, HumanRoomRobotCountryPool{CountryCode: countryCode, RobotUserIDs: ids})
	}
	return pools, nil
}

func (s *Service) startHumanRoomRobotRuntime(parent context.Context, config HumanRoomRobotConfig, countryCode string, roomID string, pool []int64) bool {
	if parent.Err() != nil || roomID == "" || len(pool) == 0 {
		return false
	}
	s.humanRobotRuntimeMu.Lock()
	if s.humanRobotRuntimes == nil {
		s.humanRobotRuntimes = make(map[string]robotRoomRuntime)
	}
	if _, exists := s.humanRobotRuntimes[roomID]; exists {
		s.humanRobotRuntimeMu.Unlock()
		return false
	}
	s.humanRobotRuntimeMu.Unlock()

	roomCtx := appcode.WithContext(parent, config.AppCode)
	targetOnline := randomHumanRoomTargetOnline(config)
	selected, err := s.prepareHumanRoomRobots(roomCtx, config, roomID, pool, targetOnline)
	if err != nil {
		logx.Warn(roomCtx, "human_room_robot_prepare_failed", slog.String("room_id", roomID), slog.String("country_code", countryCode), slog.String("error", err.Error()))
		return false
	}
	if len(selected) == 0 {
		return false
	}

	s.humanRobotRuntimeMu.Lock()
	if _, exists := s.humanRobotRuntimes[roomID]; exists {
		s.humanRobotRuntimeMu.Unlock()
		return false
	}
	ctx, cancel := context.WithCancel(roomCtx)
	token := fmt.Sprintf("%s:%d", roomID, time.Now().UTC().UnixNano())
	s.humanRobotRuntimes[roomID] = robotRoomRuntime{cancel: cancel, token: token, countryCode: countryCode, configUpdatedAtMS: config.UpdatedAtMS}
	s.humanRobotRuntimeMu.Unlock()

	logx.Info(ctx, "human_room_robot_runtime_started",
		slog.String("room_id", roomID),
		slog.String("country_code", countryCode),
		slog.Int("target_online", targetOnline),
		slog.Int("active_robot_count", len(selected)),
	)
	go func() {
		defer s.clearHumanRoomRobotRuntime(roomID, token)
		s.runHumanRoomRobotRuntime(ctx, config, roomID, pool, selected, targetOnline)
	}()
	return true
}

func (s *Service) prepareHumanRoomRobots(ctx context.Context, config HumanRoomRobotConfig, roomID string, pool []int64, targetOnline int) ([]int64, error) {
	snapshot, err := s.currentSnapshot(ctx, roomID)
	if err != nil {
		return nil, err
	}
	if snapshot.GetStatus() != "active" {
		return nil, nil
	}
	online := make(map[int64]bool, len(snapshot.GetOnlineUsers()))
	for _, user := range snapshot.GetOnlineUsers() {
		online[user.GetUserId()] = true
	}
	humanOnline := humanRoomRealOnlineCount(snapshot.GetOnlineUsers(), allHumanRoomRobotIDs(config))
	if humanOnline >= int(config.CandidateRoomMaxOnline) || len(online) >= targetOnline {
		return nil, nil
	}
	limit := targetOnline - len(online)
	if limit <= 0 {
		return nil, nil
	}
	freeSeats := freeSeatNumbers(snapshot.GetMicSeats())
	if len(freeSeats) < limit {
		limit = len(freeSeats)
	}
	if limit <= 0 {
		return nil, nil
	}
	rng := rand.New(rand.NewSource(time.Now().UnixNano()))
	candidates := append([]int64(nil), pool...)
	rng.Shuffle(len(candidates), func(i, j int) { candidates[i], candidates[j] = candidates[j], candidates[i] })
	selected := make([]int64, 0, limit)
	for _, userID := range candidates {
		if len(selected) >= limit {
			break
		}
		if userID <= 0 || online[userID] || s.robotHasActiveRoomPresence(ctx, userID, roomID) {
			continue
		}
		seatNo := freeSeats[len(selected)]
		if err := s.joinHumanRoomRobot(ctx, roomID, userID, seatNo, fmt.Sprintf("initial:%d:%d", userID, time.Now().UTC().UnixNano())); err != nil {
			logx.Warn(ctx, "human_room_robot_initial_join_failed", slog.String("room_id", roomID), slog.Int64("robot_user_id", userID), slog.String("error", err.Error()))
			continue
		}
		selected = append(selected, userID)
	}
	return selected, nil
}

func (s *Service) runHumanRoomRobotRuntime(ctx context.Context, config HumanRoomRobotConfig, roomID string, pool []int64, initialActive []int64, targetOnline int) {
	activity := newRobotRoomActivity(pool)
	for _, userID := range initialActive {
		activity.markActive(userID)
	}
	markInitialHumanRoomGiftSenders(activity, initialActive, int(config.MaxGiftSenders))
	giftSlots := make(chan struct{}, int(max(1, int(config.MaxGiftSenders))))
	for _, userID := range pool {
		userID := userID
		go s.runHumanRobotPresenceLoop(ctx, config, roomID, pool, activity, userID, targetOnline)
		go s.runHumanRobotNormalGiftLoop(ctx, config, roomID, activity, giftSlots, userID)
		go s.runHumanRobotLuckyGiftLoop(ctx, config, roomID, activity, giftSlots, userID, config.LuckyGiftIDs, humanRobotLuckyPoolID, "lucky", 5_000_000)
		go s.runHumanRobotLuckyGiftLoop(ctx, config, roomID, activity, giftSlots, userID, config.SuperLuckyGiftIDs, humanRobotSuperLuckyPoolID, "super_lucky", 20_000_000)
	}
	go s.runHumanRobotFillLoop(ctx, config, roomID, pool, activity, targetOnline)
	<-ctx.Done()
}

func (s *Service) runHumanRobotFillLoop(ctx context.Context, config HumanRoomRobotConfig, roomID string, pool []int64, activity *robotRoomActivity, targetOnline int) {
	rng := rand.New(rand.NewSource(time.Now().UnixNano()))
	timer := time.NewTimer(time.Duration(1+rng.Intn(3)) * time.Second)
	defer timer.Stop()
	for {
		select {
		case <-ctx.Done():
			return
		case <-timer.C:
			s.fillHumanRoomRobotsOnce(ctx, config, roomID, pool, activity, rng, targetOnline)
			timer.Reset(time.Duration(5+rng.Intn(5)) * time.Second)
		}
	}
}

func (s *Service) fillHumanRoomRobotsOnce(ctx context.Context, config HumanRoomRobotConfig, roomID string, pool []int64, activity *robotRoomActivity, rng *rand.Rand, targetOnline int) {
	snapshot, err := s.currentSnapshot(ctx, roomID)
	if err != nil || snapshot.GetStatus() != "active" {
		return
	}
	humanOnline := humanRoomRealOnlineCount(snapshot.GetOnlineUsers(), allHumanRoomRobotIDs(config))
	if humanOnline >= int(config.CandidateRoomMaxOnline) || len(snapshot.GetOnlineUsers()) >= targetOnline {
		return
	}
	limit := targetOnline - len(snapshot.GetOnlineUsers())
	freeSeats := freeSeatNumbers(snapshot.GetMicSeats())
	if len(freeSeats) < limit {
		limit = len(freeSeats)
	}
	if limit <= 0 {
		return
	}
	online := make(map[int64]bool, len(snapshot.GetOnlineUsers()))
	for _, user := range snapshot.GetOnlineUsers() {
		online[user.GetUserId()] = true
	}
	candidates := append([]int64(nil), pool...)
	rng.Shuffle(len(candidates), func(i, j int) { candidates[i], candidates[j] = candidates[j], candidates[i] })
	joined := 0
	for _, userID := range candidates {
		if joined >= limit {
			return
		}
		if userID <= 0 || online[userID] || activity.isActive(userID) || s.robotHasActiveRoomPresence(ctx, userID, roomID) {
			continue
		}
		seatNo := freeSeats[joined]
		if err := s.joinHumanRoomRobot(ctx, roomID, userID, seatNo, fmt.Sprintf("fill:%d:%d", userID, time.Now().UTC().UnixNano())); err != nil {
			logx.Warn(ctx, "human_room_robot_fill_join_failed", slog.String("room_id", roomID), slog.Int64("robot_user_id", userID), slog.String("error", err.Error()))
			continue
		}
		activity.markActive(userID)
		activity.markGiftSenderWithinLimit(userID, int(config.MaxGiftSenders))
		online[userID] = true
		joined++
	}
}

func (s *Service) runHumanRobotPresenceLoop(ctx context.Context, config HumanRoomRobotConfig, roomID string, pool []int64, activity *robotRoomActivity, userID int64, targetOnline int) {
	if userID <= 0 {
		return
	}
	rng := rand.New(rand.NewSource(time.Now().UnixNano() + userID*43))
	for {
		if !activity.isActive(userID) {
			if !waitRobotRuntimeDelay(ctx, time.Duration(300+rng.Intn(700))*time.Millisecond) {
				return
			}
			continue
		}
		stay := time.Duration(randomInt64Range(rng, config.RobotStayMinMS, config.RobotStayMaxMS)) * time.Millisecond
		if !waitRobotRuntimeDelay(ctx, stay) {
			return
		}
		// 送礼名额绑定在房间内的少数机器人身上；送礼机器人离开时，补位机器人接替这个名额，保证房间内送礼者数量稳定且不会扩散到所有机器人。
		wasGiftSender := activity.isGiftSender(userID)
		if !activity.markInactive(userID) {
			continue
		}
		seatNo := s.humanRoomRobotSeatNo(ctx, roomID, userID)
		if err := s.leaveHumanRoomRobot(ctx, roomID, userID); err != nil {
			logx.Warn(ctx, "human_room_robot_leave_failed", slog.String("room_id", roomID), slog.Int64("robot_user_id", userID), slog.String("error", err.Error()))
			activity.markActive(userID)
			if wasGiftSender {
				activity.markGiftSender(userID)
			}
			continue
		}
		delay := time.Duration(randomInt64Range(rng, config.RobotReplaceMinMS, config.RobotReplaceMaxMS)) * time.Millisecond
		if !waitRobotRuntimeDelay(ctx, delay) {
			return
		}
		replacementID := s.pickHumanRoomReplacement(ctx, config, roomID, pool, activity, userID)
		if replacementID <= 0 {
			continue
		}
		if seatNo <= 0 {
			seatNo = s.firstHumanRoomFreeSeatNo(ctx, roomID)
		}
		if seatNo <= 0 || !s.humanRoomNeedsRobot(ctx, config, roomID, targetOnline) {
			continue
		}
		if err := s.joinHumanRoomRobot(ctx, roomID, replacementID, seatNo, fmt.Sprintf("replace:%d:%d", replacementID, time.Now().UTC().UnixNano())); err != nil {
			logx.Warn(ctx, "human_room_robot_replace_join_failed", slog.String("room_id", roomID), slog.Int64("replacement_user_id", replacementID), slog.String("error", err.Error()))
			continue
		}
		activity.markActive(replacementID)
		if wasGiftSender || activity.giftSenderCount() < int(config.MaxGiftSenders) {
			activity.markGiftSenderWithinLimit(replacementID, int(config.MaxGiftSenders))
		}
	}
}

func (s *Service) runHumanRobotNormalGiftLoop(ctx context.Context, config HumanRoomRobotConfig, roomID string, activity *robotRoomActivity, giftSlots chan struct{}, senderID int64) {
	if config.NormalGiftIntervalMinMS <= 0 || config.NormalGiftIntervalMaxMS <= 0 || len(config.GiftIDs) == 0 {
		return
	}
	rng := rand.New(rand.NewSource(time.Now().UnixNano() + senderID*59))
	timer := time.NewTimer(humanRoomRobotNormalGiftDelay(config, rng))
	defer timer.Stop()
	for {
		select {
		case <-ctx.Done():
			return
		case <-timer.C:
			if activity.isActive(senderID) && activity.isGiftSender(senderID) {
				targetID := randomRobotTarget(rng, activity.activeIDs(), senderID)
				giftID := randomString(rng, config.GiftIDs)
				s.sendHumanRoomRobotGiftBestEffort(ctx, config, roomID, activity, giftSlots, senderID, targetID, giftID, "", "normal", 0)
			}
			timer.Reset(humanRoomRobotNormalGiftDelay(config, rng))
		}
	}
}

func (s *Service) runHumanRobotLuckyGiftLoop(ctx context.Context, config HumanRoomRobotConfig, roomID string, activity *robotRoomActivity, giftSlots chan struct{}, senderID int64, giftIDs []string, poolID string, kind string, multiplierPPM int64) {
	if len(giftIDs) == 0 || config.LuckyComboMax <= 0 {
		return
	}
	rng := rand.New(rand.NewSource(time.Now().UnixNano() + senderID*71))
	for {
		if !activity.isActive(senderID) || !activity.isGiftSender(senderID) {
			if !waitRobotRuntimeDelay(ctx, time.Duration(300+rng.Intn(700))*time.Millisecond) {
				return
			}
			continue
		}
		combo := randomInt64Range(rng, config.LuckyComboMin, config.LuckyComboMax)
		for i := int64(0); i < combo; i++ {
			if !activity.isActive(senderID) || !activity.isGiftSender(senderID) {
				break
			}
			targetID := randomRobotTarget(rng, activity.activeIDs(), senderID)
			giftID := randomString(rng, giftIDs)
			s.sendHumanRoomRobotGiftBestEffort(ctx, config, roomID, activity, giftSlots, senderID, targetID, giftID, poolID, kind, multiplierPPM)
			if !waitRobotGiftPace(ctx, rng) {
				return
			}
		}
		pause := time.Duration(randomInt64Range(rng, config.LuckyPauseMinMS, config.LuckyPauseMaxMS)) * time.Millisecond
		if !waitRobotRuntimeDelay(ctx, pause) {
			return
		}
	}
}

func (s *Service) sendHumanRoomRobotGiftBestEffort(ctx context.Context, config HumanRoomRobotConfig, roomID string, activity *robotRoomActivity, giftSlots chan struct{}, senderID int64, targetID int64, giftID string, poolID string, kind string, multiplierPPM int64) {
	if !activity.isActive(senderID) || !activity.isGiftSender(senderID) || !activity.isActive(targetID) || giftID == "" {
		return
	}
	select {
	case giftSlots <- struct{}{}:
		defer func() { <-giftSlots }()
	default:
		return
	}
	_, err := s.RobotSendGift(ctx, RobotSendGiftInput{
		Meta:                   humanRoomRobotCommandMeta(config, roomID, senderID, fmt.Sprintf("gift:%s:%d:%d", kind, targetID, time.Now().UTC().UnixNano())),
		TargetUserID:           targetID,
		GiftID:                 giftID,
		GiftCount:              1,
		PoolID:                 poolID,
		RobotUserIDs:           activity.pool,
		SyntheticMultiplierPPM: multiplierPPM,
		RealRoomHeat:           true,
	})
	if err != nil {
		reconcileHumanRoomRobotGiftPresence(activity, senderID, targetID, err)
		logx.Warn(ctx, "human_room_robot_send_gift_failed", slog.String("room_id", roomID), slog.String("kind", kind), slog.Int64("sender_user_id", senderID), slog.Int64("target_user_id", targetID), slog.String("gift_id", giftID), slog.String("error", err.Error()))
	}
}

func reconcileHumanRoomRobotGiftPresence(activity *robotRoomActivity, senderID int64, targetID int64, err error) {
	if activity == nil || err == nil || xerr.CodeOf(err) != xerr.NotFound {
		return
	}
	message := xerr.MessageOf(err)
	switch {
	case strings.Contains(message, "sender not in room"):
		// 送礼前不额外读 snapshot，避免把每次礼物尝试都放大成一次 Room Cell/恢复查询；
		// 直接复用 RobotSendGift 在 Room Cell 内的强一致校验结果，把已离房 sender 从运行时活跃集合移除。
		activity.markInactive(senderID)
	case strings.Contains(message, "target not in room"):
		// target 已经不在 Room Cell presence 时继续保留 active 会造成后续随机命中反复失败；
		// 这里只更新本地 human-room 运行时状态，补位仍由 fill/presence 循环按原有节奏执行。
		activity.markInactive(targetID)
	}
}

func markInitialHumanRoomGiftSenders(activity *robotRoomActivity, initialActive []int64, maxSenders int) {
	if activity == nil || maxSenders <= 0 || len(initialActive) == 0 {
		return
	}
	rng := rand.New(rand.NewSource(time.Now().UnixNano()))
	candidates := append([]int64(nil), initialActive...)
	rng.Shuffle(len(candidates), func(i int, j int) {
		candidates[i], candidates[j] = candidates[j], candidates[i]
	})
	for _, userID := range candidates {
		if activity.giftSenderCount() >= maxSenders {
			return
		}
		activity.markGiftSenderWithinLimit(userID, maxSenders)
	}
}

func humanRoomRobotNormalGiftDelay(config HumanRoomRobotConfig, rng *rand.Rand) time.Duration {
	minMS := config.NormalGiftIntervalMinMS
	maxMS := config.NormalGiftIntervalMaxMS
	if minMS <= 0 && maxMS <= 0 && config.NormalGiftIntervalMS > 0 {
		minMS = config.NormalGiftIntervalMS
		maxMS = config.NormalGiftIntervalMS
	}
	if minMS <= 0 {
		minMS = defaultHumanRobotNormalGiftIntervalMinMS
	}
	if maxMS < minMS {
		maxMS = minMS
	}
	return time.Duration(randomInt64Range(rng, minMS, maxMS)) * time.Millisecond
}

func randomHumanRoomTargetOnline(config HumanRoomRobotConfig) int {
	minOnline := config.RoomTargetMinOnline
	maxOnline := config.RoomTargetMaxOnline
	if minOnline <= 0 && maxOnline <= 0 && config.RoomFullStopOnline > 0 {
		minOnline = config.RoomFullStopOnline
		maxOnline = config.RoomFullStopOnline
	}
	if minOnline <= 0 {
		minOnline = defaultHumanRobotRoomTargetMinOnline
	}
	if maxOnline < minOnline {
		maxOnline = minOnline
	}
	rng := rand.New(rand.NewSource(time.Now().UnixNano()))
	return int(randomInt64Range(rng, int64(minOnline), int64(maxOnline)))
}

func (s *Service) joinHumanRoomRobot(ctx context.Context, roomID string, userID int64, seatNo int32, suffix string) error {
	if _, err := s.JoinRoom(ctx, &roomv1.JoinRoomRequest{
		Meta:         humanRoomRobotCommandMeta(HumanRoomRobotConfig{AppCode: appcode.FromContext(ctx)}, roomID, userID, "join:"+suffix),
		Role:         "audience",
		ActorIsRobot: true,
	}); err != nil {
		return err
	}
	if _, err := s.RobotVirtualMicUp(ctx, RobotMicUpInput{
		Meta:         humanRoomRobotCommandMeta(HumanRoomRobotConfig{AppCode: appcode.FromContext(ctx)}, roomID, userID, "mic:"+suffix),
		TargetUserID: userID,
		SeatNo:       seatNo,
	}); err != nil {
		if xerr.CodeOf(err) == xerr.Conflict && strings.Contains(xerr.MessageOf(err), "already on seat") {
			return nil
		}
		return err
	}
	return nil
}

func (s *Service) leaveHumanRoomRobot(ctx context.Context, roomID string, userID int64) error {
	_, err := s.LeaveRoom(ctx, &roomv1.LeaveRoomRequest{
		Meta: humanRoomRobotCommandMeta(HumanRoomRobotConfig{AppCode: appcode.FromContext(ctx)}, roomID, userID, fmt.Sprintf("leave:%d:%d", userID, time.Now().UTC().UnixNano())),
	})
	return err
}

func (s *Service) pickHumanRoomReplacement(ctx context.Context, config HumanRoomRobotConfig, roomID string, pool []int64, activity *robotRoomActivity, departingUserID int64) int64 {
	active := make(map[int64]bool, len(activity.activeIDs()))
	for _, id := range activity.activeIDs() {
		active[id] = true
	}
	rng := rand.New(rand.NewSource(time.Now().UnixNano()))
	candidates := append([]int64(nil), pool...)
	rng.Shuffle(len(candidates), func(i, j int) { candidates[i], candidates[j] = candidates[j], candidates[i] })
	for _, userID := range candidates {
		if userID <= 0 || active[userID] || userID == departingUserID || s.robotHasActiveRoomPresence(ctx, userID, roomID) {
			continue
		}
		return userID
	}
	if !s.robotHasActiveRoomPresence(ctx, departingUserID, roomID) {
		return departingUserID
	}
	return 0
}

func (s *Service) humanRoomNeedsRobot(ctx context.Context, config HumanRoomRobotConfig, roomID string, targetOnline int) bool {
	snapshot, err := s.currentSnapshot(ctx, roomID)
	if err != nil || snapshot.GetStatus() != "active" {
		return false
	}
	humanOnline := humanRoomRealOnlineCount(snapshot.GetOnlineUsers(), allHumanRoomRobotIDs(config))
	return humanOnline < int(config.CandidateRoomMaxOnline) && len(snapshot.GetOnlineUsers()) < targetOnline
}

func (s *Service) robotHasActiveRoomPresence(ctx context.Context, userID int64, allowedRoomID string) bool {
	presence, exists, err := s.repository.GetCurrentRoomPresence(ctx, userID)
	if err != nil || !exists {
		return false
	}
	return presence.RoomID != "" && presence.RoomID != allowedRoomID
}

func (s *Service) humanRoomRobotSeatNo(ctx context.Context, roomID string, userID int64) int32 {
	snapshot, err := s.currentSnapshot(ctx, roomID)
	if err != nil {
		return 0
	}
	for _, seat := range snapshot.GetMicSeats() {
		if seat.GetUserId() == userID {
			return seat.GetSeatNo()
		}
	}
	return 0
}

func (s *Service) firstHumanRoomFreeSeatNo(ctx context.Context, roomID string) int32 {
	snapshot, err := s.currentSnapshot(ctx, roomID)
	if err != nil {
		return 0
	}
	seats := freeSeatNumbers(snapshot.GetMicSeats())
	if len(seats) == 0 {
		return 0
	}
	return seats[0]
}

func (s *Service) reconcileHumanRoomRobotRuntimeLimits(config HumanRoomRobotConfig, countryLimits map[string]int) map[string]int {
	counts := make(map[string]int, len(countryLimits))
	cancels := make([]context.CancelFunc, 0)
	s.humanRobotRuntimeMu.Lock()
	for roomID, runtime := range s.humanRobotRuntimes {
		countryCode := normalizeRoomCountryCode(runtime.countryCode)
		// 配置版本变化后直接取消旧 runtime，保证停留、礼物、国家限额等行为不会继续沿用旧快照。
		if runtime.configUpdatedAtMS > 0 && config.UpdatedAtMS > 0 && runtime.configUpdatedAtMS != config.UpdatedAtMS {
			if runtime.cancel != nil {
				cancels = append(cancels, runtime.cancel)
			}
			delete(s.humanRobotRuntimes, roomID)
			continue
		}
		// 国家限额开启后，未配置国家和超过本国家房间上限的 runtime 都必须停掉，避免旧扫描结果继续占用真人房。
		if config.CountryLimitEnabled {
			limit := countryLimits[countryCode]
			if countryCode == "" || limit <= 0 || counts[countryCode] >= limit {
				if runtime.cancel != nil {
					cancels = append(cancels, runtime.cancel)
				}
				delete(s.humanRobotRuntimes, roomID)
				continue
			}
		}
		counts[countryCode]++
	}
	s.humanRobotRuntimeMu.Unlock()
	for _, cancel := range cancels {
		cancel()
	}
	return counts
}

func (s *Service) humanRoomRobotRuntimeExists(roomID string) bool {
	s.humanRobotRuntimeMu.Lock()
	defer s.humanRobotRuntimeMu.Unlock()
	_, exists := s.humanRobotRuntimes[roomID]
	return exists
}

func (s *Service) humanRoomRobotRuntimeConfigChanged(roomID string, updatedAtMS int64) bool {
	if updatedAtMS <= 0 {
		return false
	}
	s.humanRobotRuntimeMu.Lock()
	defer s.humanRobotRuntimeMu.Unlock()
	runtime, exists := s.humanRobotRuntimes[roomID]
	return exists && runtime.configUpdatedAtMS > 0 && runtime.configUpdatedAtMS != updatedAtMS
}

func (s *Service) stopHumanRoomRobotRuntime(roomID string) {
	s.humanRobotRuntimeMu.Lock()
	runtime := s.humanRobotRuntimes[roomID]
	delete(s.humanRobotRuntimes, roomID)
	s.humanRobotRuntimeMu.Unlock()
	if runtime.cancel != nil {
		runtime.cancel()
	}
}

func (s *Service) clearHumanRoomRobotRuntime(roomID string, token string) {
	s.humanRobotRuntimeMu.Lock()
	defer s.humanRobotRuntimeMu.Unlock()
	runtime, exists := s.humanRobotRuntimes[roomID]
	if !exists || runtime.token != token {
		return
	}
	delete(s.humanRobotRuntimes, roomID)
}

func (s *Service) stopAllHumanRoomRobotRuntimes() {
	s.humanRobotRuntimeMu.Lock()
	cancels := make([]context.CancelFunc, 0, len(s.humanRobotRuntimes))
	for roomID, runtime := range s.humanRobotRuntimes {
		if runtime.cancel != nil {
			cancels = append(cancels, runtime.cancel)
		}
		delete(s.humanRobotRuntimes, roomID)
	}
	s.humanRobotRuntimeMu.Unlock()
	for _, cancel := range cancels {
		cancel()
	}
}

func normalizeHumanRoomRobotConfig(appCode string, input *roomv1.AdminHumanRoomRobotConfig, adminID uint64, nowMS int64) (HumanRoomRobotConfig, error) {
	if input == nil {
		return HumanRoomRobotConfig{}, xerr.New(xerr.InvalidArgument, "human room robot config is required")
	}
	countryRules, err := normalizeHumanRoomRobotCountryRules(input.GetCountryRules())
	if err != nil {
		return HumanRoomRobotConfig{}, err
	}
	config := HumanRoomRobotConfig{
		AppCode:                 appcode.Normalize(appCode),
		Enabled:                 input.GetEnabled(),
		CandidateRoomMaxOnline:  input.GetCandidateRoomMaxOnline(),
		RoomFullStopOnline:      input.GetRoomFullStopOnline(),
		RoomTargetMinOnline:     input.GetRoomTargetMinOnline(),
		RoomTargetMaxOnline:     input.GetRoomTargetMaxOnline(),
		RobotStayMinMS:          input.GetRobotStayMinMs(),
		RobotStayMaxMS:          input.GetRobotStayMaxMs(),
		RobotReplaceMinMS:       input.GetRobotReplaceMinMs(),
		RobotReplaceMaxMS:       input.GetRobotReplaceMaxMs(),
		NormalGiftIntervalMS:    input.GetNormalGiftIntervalMs(),
		NormalGiftIntervalMinMS: input.GetNormalGiftIntervalMinMs(),
		NormalGiftIntervalMaxMS: input.GetNormalGiftIntervalMaxMs(),
		GiftIDs:                 normalizeStringSet(input.GetGiftIds()),
		LuckyGiftIDs:            normalizeStringSet(input.GetLuckyGiftIds()),
		SuperLuckyGiftIDs:       normalizeStringSet(input.GetSuperLuckyGiftIds()),
		LuckyComboMin:           input.GetLuckyComboMin(),
		LuckyComboMax:           input.GetLuckyComboMax(),
		LuckyPauseMinMS:         input.GetLuckyPauseMinMs(),
		LuckyPauseMaxMS:         input.GetLuckyPauseMaxMs(),
		MaxGiftSenders:          input.GetMaxGiftSenders(),
		AllowedOwnerIDs:         normalizeStringSet(input.GetAllowedOwnerIds()),
		CountryLimitEnabled:     input.GetCountryLimitEnabled(),
		CountryRules:            countryRules,
		UpdatedByAdminID:        adminID,
		CreatedAtMS:             input.GetCreatedAtMs(),
		UpdatedAtMS:             nowMS,
	}
	config = withHumanRoomRobotDefaults(config)
	if config.CandidateRoomMaxOnline <= 0 || config.RoomTargetMinOnline <= 0 || config.RoomTargetMaxOnline < config.RoomTargetMinOnline {
		return HumanRoomRobotConfig{}, xerr.New(xerr.InvalidArgument, "room online thresholds are invalid")
	}
	if config.RobotStayMinMS <= 0 || config.RobotStayMaxMS < config.RobotStayMinMS {
		return HumanRoomRobotConfig{}, xerr.New(xerr.InvalidArgument, "robot stay range is invalid")
	}
	if config.RobotReplaceMinMS < 0 || config.RobotReplaceMaxMS < config.RobotReplaceMinMS {
		return HumanRoomRobotConfig{}, xerr.New(xerr.InvalidArgument, "robot replace range is invalid")
	}
	if config.NormalGiftIntervalMinMS <= 0 || config.NormalGiftIntervalMaxMS < config.NormalGiftIntervalMinMS {
		return HumanRoomRobotConfig{}, xerr.New(xerr.InvalidArgument, "normal gift interval range is invalid")
	}
	if config.Enabled && len(config.GiftIDs) == 0 {
		return HumanRoomRobotConfig{}, xerr.New(xerr.InvalidArgument, "gift_ids is required")
	}
	if config.Enabled && len(config.LuckyGiftIDs) == 0 && len(config.SuperLuckyGiftIDs) == 0 {
		return HumanRoomRobotConfig{}, xerr.New(xerr.InvalidArgument, "lucky gift ids are required")
	}
	if config.LuckyComboMin <= 0 || config.LuckyComboMax < config.LuckyComboMin {
		return HumanRoomRobotConfig{}, xerr.New(xerr.InvalidArgument, "lucky combo range is invalid")
	}
	if config.LuckyPauseMinMS <= 0 || config.LuckyPauseMaxMS < config.LuckyPauseMinMS {
		return HumanRoomRobotConfig{}, xerr.New(xerr.InvalidArgument, "lucky pause range is invalid")
	}
	if config.MaxGiftSenders <= 0 {
		return HumanRoomRobotConfig{}, xerr.New(xerr.InvalidArgument, "max_gift_senders is required")
	}
	if config.Enabled && config.CountryLimitEnabled && len(config.CountryRules) == 0 {
		return HumanRoomRobotConfig{}, xerr.New(xerr.InvalidArgument, "country_rules is required")
	}
	return config, nil
}

func withHumanRoomRobotDefaults(config HumanRoomRobotConfig) HumanRoomRobotConfig {
	if config.AppCode == "" {
		config.AppCode = appcode.Default
	}
	if config.CandidateRoomMaxOnline <= 0 {
		config.CandidateRoomMaxOnline = defaultHumanRobotCandidateRoomMaxOnline
	}
	if config.RoomTargetMinOnline <= 0 && config.RoomTargetMaxOnline <= 0 {
		if config.RoomFullStopOnline > 0 {
			config.RoomTargetMinOnline = config.RoomFullStopOnline
			config.RoomTargetMaxOnline = config.RoomFullStopOnline
		} else {
			config.RoomTargetMinOnline = defaultHumanRobotRoomTargetMinOnline
			config.RoomTargetMaxOnline = defaultHumanRobotRoomTargetMaxOnline
		}
	}
	if config.RoomFullStopOnline <= 0 {
		config.RoomFullStopOnline = config.RoomTargetMaxOnline
	}
	if config.RobotStayMinMS <= 0 && config.RobotStayMaxMS <= 0 {
		config.RobotStayMinMS = defaultHumanRobotStayMinMS
		config.RobotStayMaxMS = defaultHumanRobotStayMaxMS
	}
	if config.RobotReplaceMinMS == 0 && config.RobotReplaceMaxMS == 0 {
		config.RobotReplaceMinMS = defaultHumanRobotReplaceMinMS
		config.RobotReplaceMaxMS = defaultHumanRobotReplaceMaxMS
	}
	if config.NormalGiftIntervalMinMS <= 0 && config.NormalGiftIntervalMaxMS <= 0 {
		if config.NormalGiftIntervalMS > 0 {
			config.NormalGiftIntervalMinMS = config.NormalGiftIntervalMS
			config.NormalGiftIntervalMaxMS = config.NormalGiftIntervalMS
		} else {
			config.NormalGiftIntervalMinMS = defaultHumanRobotNormalGiftIntervalMinMS
			config.NormalGiftIntervalMaxMS = defaultHumanRobotNormalGiftIntervalMaxMS
		}
	}
	if config.NormalGiftIntervalMS <= 0 {
		config.NormalGiftIntervalMS = config.NormalGiftIntervalMaxMS
	}
	if config.LuckyComboMin <= 0 && config.LuckyComboMax <= 0 {
		config.LuckyComboMin = defaultHumanRobotLuckyComboMin
		config.LuckyComboMax = defaultHumanRobotLuckyComboMax
	}
	if config.LuckyPauseMinMS <= 0 && config.LuckyPauseMaxMS <= 0 {
		config.LuckyPauseMinMS = defaultHumanRobotLuckyPauseMinMS
		config.LuckyPauseMaxMS = defaultHumanRobotLuckyPauseMaxMS
	}
	if config.MaxGiftSenders <= 0 {
		config.MaxGiftSenders = defaultHumanRobotMaxGiftSenders
	}
	return config
}

func defaultHumanRoomRobotConfig(appCode string, nowMS int64) HumanRoomRobotConfig {
	return withHumanRoomRobotDefaults(HumanRoomRobotConfig{AppCode: appcode.Normalize(appCode), CreatedAtMS: nowMS, UpdatedAtMS: nowMS})
}

func normalizeHumanRoomRobotCountryRules(values []*roomv1.AdminHumanRoomRobotCountryRule) ([]HumanRoomRobotCountryRule, error) {
	seen := make(map[string]bool, len(values))
	rules := make([]HumanRoomRobotCountryRule, 0, len(values))
	for _, value := range values {
		countryCode := normalizeRoomCountryCode(value.GetCountryCode())
		if countryCode == "" {
			return nil, xerr.New(xerr.InvalidArgument, "country_code is invalid")
		}
		if seen[countryCode] {
			return nil, xerr.New(xerr.InvalidArgument, "country_code is duplicated")
		}
		if value.GetMaxRoomCount() <= 0 {
			return nil, xerr.New(xerr.InvalidArgument, "country max_room_count is invalid")
		}
		seen[countryCode] = true
		rules = append(rules, HumanRoomRobotCountryRule{
			CountryCode:     countryCode,
			MaxRoomCount:    value.GetMaxRoomCount(),
			AllowedOwnerIDs: normalizeStringSet(value.GetAllowedOwnerIds()),
		})
	}
	sort.Slice(rules, func(i, j int) bool {
		return rules[i].CountryCode < rules[j].CountryCode
	})
	return rules, nil
}

func humanRoomRobotCountryLimits(config HumanRoomRobotConfig) map[string]int {
	limits := make(map[string]int, len(config.CountryRules))
	if !config.CountryLimitEnabled {
		return limits
	}
	for _, rule := range config.CountryRules {
		countryCode := normalizeRoomCountryCode(rule.CountryCode)
		if countryCode == "" || rule.MaxRoomCount <= 0 {
			continue
		}
		// 同国家重复规则在保存阶段已被拒绝；这里保留最后一次赋值，保证历史脏数据不会导致 panic。
		limits[countryCode] = int(rule.MaxRoomCount)
	}
	return limits
}

func humanRoomRobotAllowedOwnersForCountry(config HumanRoomRobotConfig, countryCode string) []string {
	countryCode = normalizeRoomCountryCode(countryCode)
	for _, rule := range config.CountryRules {
		if normalizeRoomCountryCode(rule.CountryCode) != countryCode {
			continue
		}
		// 国家规则里填写房主 ID 时，它是当前国家的精确白名单；为空时允许继续使用历史全局白名单。
		if len(rule.AllowedOwnerIDs) > 0 {
			return append([]string(nil), rule.AllowedOwnerIDs...)
		}
		return append([]string(nil), config.AllowedOwnerIDs...)
	}
	return append([]string(nil), config.AllowedOwnerIDs...)
}

func humanRoomRobotConfigToProto(config HumanRoomRobotConfig) *roomv1.AdminHumanRoomRobotConfig {
	config = withHumanRoomRobotDefaults(config)
	out := &roomv1.AdminHumanRoomRobotConfig{
		AppCode:                 config.AppCode,
		Enabled:                 config.Enabled,
		CandidateRoomMaxOnline:  config.CandidateRoomMaxOnline,
		RoomFullStopOnline:      config.RoomFullStopOnline,
		RoomTargetMinOnline:     config.RoomTargetMinOnline,
		RoomTargetMaxOnline:     config.RoomTargetMaxOnline,
		RobotStayMinMs:          config.RobotStayMinMS,
		RobotStayMaxMs:          config.RobotStayMaxMS,
		RobotReplaceMinMs:       config.RobotReplaceMinMS,
		RobotReplaceMaxMs:       config.RobotReplaceMaxMS,
		NormalGiftIntervalMs:    config.NormalGiftIntervalMS,
		NormalGiftIntervalMinMs: config.NormalGiftIntervalMinMS,
		NormalGiftIntervalMaxMs: config.NormalGiftIntervalMaxMS,
		GiftIds:                 append([]string(nil), config.GiftIDs...),
		LuckyGiftIds:            append([]string(nil), config.LuckyGiftIDs...),
		SuperLuckyGiftIds:       append([]string(nil), config.SuperLuckyGiftIDs...),
		LuckyComboMin:           config.LuckyComboMin,
		LuckyComboMax:           config.LuckyComboMax,
		LuckyPauseMinMs:         config.LuckyPauseMinMS,
		LuckyPauseMaxMs:         config.LuckyPauseMaxMS,
		MaxGiftSenders:          config.MaxGiftSenders,
		AllowedOwnerIds:         append([]string(nil), config.AllowedOwnerIDs...),
		CountryLimitEnabled:     config.CountryLimitEnabled,
		UpdatedByAdminId:        config.UpdatedByAdminID,
		CreatedAtMs:             config.CreatedAtMS,
		UpdatedAtMs:             config.UpdatedAtMS,
	}
	for _, rule := range config.CountryRules {
		out.CountryRules = append(out.CountryRules, &roomv1.AdminHumanRoomRobotCountryRule{
			CountryCode:     rule.CountryCode,
			MaxRoomCount:    rule.MaxRoomCount,
			AllowedOwnerIds: append([]string(nil), rule.AllowedOwnerIDs...),
		})
	}
	return out
}

func humanRoomRobotCommandMeta(config HumanRoomRobotConfig, roomID string, actorUserID int64, suffix string) *roomv1.RequestMeta {
	commandID := fmt.Sprintf("human-room-robot:%s:%s", roomID, strings.TrimSpace(suffix))
	if len(commandID) > 128 {
		commandID = commandID[:128]
	}
	return &roomv1.RequestMeta{
		RequestId:   commandID,
		CommandId:   commandID,
		ActorUserId: actorUserID,
		RoomId:      roomID,
		AppCode:     config.AppCode,
		SentAtMs:    time.Now().UTC().UnixMilli(),
	}
}

func humanRoomRealOnlineCount(users []*roomv1.RoomUser, robotIDs map[int64]bool) int {
	count := 0
	for _, user := range users {
		if user.GetUserId() <= 0 || robotIDs[user.GetUserId()] {
			continue
		}
		count++
	}
	return count
}

func allHumanRoomRobotIDs(config HumanRoomRobotConfig) map[int64]bool {
	out := make(map[int64]bool)
	for _, pool := range config.CountryPools {
		for _, id := range pool.RobotUserIDs {
			if id > 0 {
				out[id] = true
			}
		}
	}
	return out
}

func freeSeatNumbers(seats []*roomv1.SeatState) []int32 {
	out := make([]int32, 0, len(seats))
	for _, seat := range seats {
		if seat.GetSeatNo() > 0 && seat.GetUserId() == 0 && !seat.GetLocked() {
			out = append(out, seat.GetSeatNo())
		}
	}
	return out
}
