// Package mysql 实现 room-service 的 MySQL 持久化边界。
package mysql

import (
	"context"
	"database/sql"
	"encoding/json"
	"errors"
	"strconv"
	"strings"
	"time"

	"hyapp/pkg/appcode"
	"hyapp/pkg/xerr"
	roomservice "hyapp/services/room-service/internal/room/service"
)

// ListRobotRooms 读取后台机器人房间列表；机器人房间配置归 room-service 所有，后台不直连房间库。
func (r *Repository) ListRobotRooms(ctx context.Context, query roomservice.RobotRoomListQuery) ([]roomservice.RobotRoomConfig, int64, error) {
	appCode := normalizedRecordAppCode(ctx, query.AppCode)
	page := query.Page
	if page <= 0 {
		page = 1
	}
	pageSize := query.PageSize
	if pageSize <= 0 || pageSize > 100 {
		pageSize = 50
	}
	status := strings.TrimSpace(query.Status)
	args := []any{appCode}
	where := `WHERE app_code = ?`
	if status != "" {
		where += ` AND status = ?`
		args = append(args, status)
	}
	var total int64
	if err := r.db.QueryRowContext(ctx, `SELECT COUNT(*) FROM room_robot_rooms `+where, args...).Scan(&total); err != nil {
		return nil, 0, err
	}
	args = append(args, pageSize, (page-1)*pageSize)
	rows, err := r.db.QueryContext(ctx,
		`SELECT app_code, room_id, room_short_id, title, cover_url, visible_region_id, status,
		        owner_robot_user_id, robot_user_ids_json, active_robot_count, seat_count, gift_ids_json, lucky_gift_ids_json,
		        normal_gift_interval_ms, lucky_combo_min, lucky_combo_max, lucky_pause_min_ms, lucky_pause_max_ms,
		        robot_stay_min_ms, robot_stay_max_ms, robot_replace_min_ms, robot_replace_max_ms, max_gift_senders,
		        created_by_admin_id, created_at_ms, updated_at_ms
		   FROM room_robot_rooms `+where+`
		  ORDER BY updated_at_ms DESC, room_id DESC
		  LIMIT ? OFFSET ?`,
		args...,
	)
	if err != nil {
		return nil, 0, err
	}
	defer rows.Close()
	items, err := scanRobotRoomConfigs(rows)
	return items, total, err
}

// GetRobotRoom 精确读取一个机器人房间配置。
func (r *Repository) GetRobotRoom(ctx context.Context, roomID string) (roomservice.RobotRoomConfig, bool, error) {
	row := r.db.QueryRowContext(ctx,
		`SELECT app_code, room_id, room_short_id, title, cover_url, visible_region_id, status,
		        owner_robot_user_id, robot_user_ids_json, active_robot_count, seat_count, gift_ids_json, lucky_gift_ids_json,
		        normal_gift_interval_ms, lucky_combo_min, lucky_combo_max, lucky_pause_min_ms, lucky_pause_max_ms,
		        robot_stay_min_ms, robot_stay_max_ms, robot_replace_min_ms, robot_replace_max_ms, max_gift_senders,
		        created_by_admin_id, created_at_ms, updated_at_ms
		   FROM room_robot_rooms
		  WHERE app_code = ? AND room_id = ?`,
		appcode.FromContext(ctx),
		strings.TrimSpace(roomID),
	)
	item, err := scanRobotRoomConfig(row)
	if errors.Is(err, sql.ErrNoRows) {
		return roomservice.RobotRoomConfig{}, false, nil
	}
	return item, err == nil, err
}

// CreateRobotRoomConfig 写入机器人房间配置，并用事务锁定候选账号避免两个 active 机器人房复用同一批账号。
func (r *Repository) CreateRobotRoomConfig(ctx context.Context, input roomservice.CreateRobotRoomConfigInput) (roomservice.RobotRoomConfig, error) {
	config := input.Config
	appCode := normalizedRecordAppCode(ctx, config.AppCode)
	nowMS := input.NowMS
	if nowMS <= 0 {
		nowMS = time.Now().UTC().UnixMilli()
	}
	config.AppCode = appCode
	config.CreatedAtMS = firstPositiveInt64(config.CreatedAtMS, nowMS)
	config.UpdatedAtMS = firstPositiveInt64(config.UpdatedAtMS, nowMS)
	if config.Status == "" {
		config.Status = "active"
	}
	rawRobots, rawGifts, rawLuckyGifts, err := robotRoomJSON(config)
	if err != nil {
		return roomservice.RobotRoomConfig{}, err
	}
	tx, err := r.db.BeginTx(ctx, nil)
	if err != nil {
		return roomservice.RobotRoomConfig{}, err
	}
	defer func() { _ = tx.Rollback() }()
	occupied, err := occupiedRobotUserIDsTx(ctx, tx, appCode, config.RobotUserIDs)
	if err != nil {
		return roomservice.RobotRoomConfig{}, err
	}
	if len(occupied) > 0 {
		return roomservice.RobotRoomConfig{}, xerr.New(xerr.Conflict, "robot user already assigned to active robot room")
	}
	_, err = tx.ExecContext(ctx,
		`INSERT INTO room_robot_rooms (
		   app_code, room_id, room_short_id, title, cover_url, visible_region_id, status,
		   owner_robot_user_id, robot_user_ids_json, active_robot_count, seat_count, gift_ids_json, lucky_gift_ids_json,
		   normal_gift_interval_ms, lucky_combo_min, lucky_combo_max, lucky_pause_min_ms, lucky_pause_max_ms,
		   robot_stay_min_ms, robot_stay_max_ms, robot_replace_min_ms, robot_replace_max_ms, max_gift_senders,
		   created_by_admin_id, created_at_ms, updated_at_ms
		 ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
		config.AppCode,
		config.RoomID,
		config.RoomShortID,
		config.Title,
		config.CoverURL,
		config.VisibleRegionID,
		config.Status,
		config.OwnerRobotUserID,
		string(rawRobots),
		config.ActiveRobotCount,
		config.SeatCount,
		string(rawGifts),
		string(rawLuckyGifts),
		config.GiftRule.NormalGiftIntervalMS,
		config.GiftRule.LuckyComboMin,
		config.GiftRule.LuckyComboMax,
		config.GiftRule.LuckyPauseMinMS,
		config.GiftRule.LuckyPauseMaxMS,
		config.GiftRule.RobotStayMinMS,
		config.GiftRule.RobotStayMaxMS,
		config.GiftRule.RobotReplaceMinMS,
		config.GiftRule.RobotReplaceMaxMS,
		config.GiftRule.MaxGiftSenders,
		config.CreatedByAdminID,
		config.CreatedAtMS,
		config.UpdatedAtMS,
	)
	if err != nil {
		return roomservice.RobotRoomConfig{}, err
	}
	if err := tx.Commit(); err != nil {
		return roomservice.RobotRoomConfig{}, err
	}
	return config, nil
}

// UpdateRobotRoomStatus 只切换机器人房间运行态；停止后账号会从“已占用”集合释放。
func (r *Repository) UpdateRobotRoomStatus(ctx context.Context, roomID string, status string, nowMS int64) (roomservice.RobotRoomConfig, bool, error) {
	status = strings.TrimSpace(status)
	if nowMS <= 0 {
		nowMS = time.Now().UTC().UnixMilli()
	}
	res, err := r.db.ExecContext(ctx,
		`UPDATE room_robot_rooms
		    SET status = ?, updated_at_ms = ?
		  WHERE app_code = ? AND room_id = ?`,
		status,
		nowMS,
		appcode.FromContext(ctx),
		strings.TrimSpace(roomID),
	)
	if err != nil {
		return roomservice.RobotRoomConfig{}, false, err
	}
	affected, _ := res.RowsAffected()
	if affected == 0 {
		return roomservice.RobotRoomConfig{}, false, nil
	}
	item, exists, err := r.GetRobotRoom(ctx, roomID)
	return item, exists, err
}

// OccupiedRobotUserIDs 返回仍被 active 机器人房占用的机器人账号集合。
func (r *Repository) OccupiedRobotUserIDs(ctx context.Context, userIDs []int64) (map[int64]bool, error) {
	return occupiedRobotUserIDsQuery(ctx, r.db, appcode.FromContext(ctx), userIDs, false)
}

// ListActiveRobotRooms 为 room-service 重启恢复运行时循环提供当前 active 配置。
func (r *Repository) ListActiveRobotRooms(ctx context.Context) ([]roomservice.RobotRoomConfig, error) {
	rows, err := r.db.QueryContext(ctx,
		`SELECT app_code, room_id, room_short_id, title, cover_url, visible_region_id, status,
		        owner_robot_user_id, robot_user_ids_json, active_robot_count, seat_count, gift_ids_json, lucky_gift_ids_json,
		        normal_gift_interval_ms, lucky_combo_min, lucky_combo_max, lucky_pause_min_ms, lucky_pause_max_ms,
		        robot_stay_min_ms, robot_stay_max_ms, robot_replace_min_ms, robot_replace_max_ms, max_gift_senders,
		        created_by_admin_id, created_at_ms, updated_at_ms
		   FROM room_robot_rooms
		  WHERE status = 'active'
		  ORDER BY app_code ASC, updated_at_ms DESC, room_id DESC`,
	)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	return scanRobotRoomConfigs(rows)
}

// GetHumanRoomRobotConfig 读取真人房间机器人行为配置；没有配置时由 service 层补默认值。
func (r *Repository) GetHumanRoomRobotConfig(ctx context.Context) (roomservice.HumanRoomRobotConfig, bool, error) {
	row := r.db.QueryRowContext(ctx,
		`SELECT app_code, enabled, candidate_room_max_online, room_full_stop_online, room_target_min_online, room_target_max_online,
		        robot_stay_min_ms, robot_stay_max_ms, robot_replace_min_ms, robot_replace_max_ms,
		        normal_gift_interval_ms, normal_gift_interval_min_ms, normal_gift_interval_max_ms,
		        gift_ids_json, lucky_gift_ids_json, super_lucky_gift_ids_json,
		        lucky_combo_min, lucky_combo_max, lucky_pause_min_ms, lucky_pause_max_ms, max_gift_senders,
		        country_pools_json, COALESCE(allowed_owner_ids_json, JSON_ARRAY()),
		        country_limit_enabled, COALESCE(country_rules_json, JSON_ARRAY()),
		        updated_by_admin_id, created_at_ms, updated_at_ms
		   FROM room_human_robot_configs
		  WHERE app_code = ?`,
		appcode.FromContext(ctx),
	)
	config, err := scanHumanRoomRobotConfig(row)
	if errors.Is(err, sql.ErrNoRows) {
		return roomservice.HumanRoomRobotConfig{}, false, nil
	}
	return config, err == nil, err
}

// UpsertHumanRoomRobotConfig 保存真人房间机器人行为配置；运行时扫描每轮读取最新配置。
func (r *Repository) UpsertHumanRoomRobotConfig(ctx context.Context, config roomservice.HumanRoomRobotConfig) (roomservice.HumanRoomRobotConfig, error) {
	appCode := normalizedRecordAppCode(ctx, config.AppCode)
	nowMS := config.UpdatedAtMS
	if nowMS <= 0 {
		nowMS = time.Now().UTC().UnixMilli()
	}
	config.AppCode = appCode
	config.CreatedAtMS = firstPositiveInt64(config.CreatedAtMS, nowMS)
	config.UpdatedAtMS = nowMS
	rawGifts, rawLucky, rawSuperLucky, rawPools, rawAllowedOwners, rawCountryRules, err := humanRoomRobotConfigJSON(config)
	if err != nil {
		return roomservice.HumanRoomRobotConfig{}, err
	}
	_, err = r.db.ExecContext(ctx,
		`INSERT INTO room_human_robot_configs (
		   app_code, enabled, candidate_room_max_online, room_full_stop_online, room_target_min_online, room_target_max_online,
		   robot_stay_min_ms, robot_stay_max_ms, robot_replace_min_ms, robot_replace_max_ms,
		   normal_gift_interval_ms, normal_gift_interval_min_ms, normal_gift_interval_max_ms,
		   gift_ids_json, lucky_gift_ids_json, super_lucky_gift_ids_json,
		   lucky_combo_min, lucky_combo_max, lucky_pause_min_ms, lucky_pause_max_ms, max_gift_senders,
		   country_pools_json, allowed_owner_ids_json, country_limit_enabled, country_rules_json,
		   updated_by_admin_id, created_at_ms, updated_at_ms
		 ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
		 ON DUPLICATE KEY UPDATE
		   enabled = VALUES(enabled),
		   candidate_room_max_online = VALUES(candidate_room_max_online),
		   room_full_stop_online = VALUES(room_full_stop_online),
		   room_target_min_online = VALUES(room_target_min_online),
		   room_target_max_online = VALUES(room_target_max_online),
		   robot_stay_min_ms = VALUES(robot_stay_min_ms),
		   robot_stay_max_ms = VALUES(robot_stay_max_ms),
		   robot_replace_min_ms = VALUES(robot_replace_min_ms),
		   robot_replace_max_ms = VALUES(robot_replace_max_ms),
		   normal_gift_interval_ms = VALUES(normal_gift_interval_ms),
		   normal_gift_interval_min_ms = VALUES(normal_gift_interval_min_ms),
		   normal_gift_interval_max_ms = VALUES(normal_gift_interval_max_ms),
		   gift_ids_json = VALUES(gift_ids_json),
		   lucky_gift_ids_json = VALUES(lucky_gift_ids_json),
		   super_lucky_gift_ids_json = VALUES(super_lucky_gift_ids_json),
		   lucky_combo_min = VALUES(lucky_combo_min),
		   lucky_combo_max = VALUES(lucky_combo_max),
		   lucky_pause_min_ms = VALUES(lucky_pause_min_ms),
		   lucky_pause_max_ms = VALUES(lucky_pause_max_ms),
		   max_gift_senders = VALUES(max_gift_senders),
		   country_pools_json = VALUES(country_pools_json),
		   allowed_owner_ids_json = VALUES(allowed_owner_ids_json),
		   country_limit_enabled = VALUES(country_limit_enabled),
		   country_rules_json = VALUES(country_rules_json),
		   updated_by_admin_id = VALUES(updated_by_admin_id),
		   updated_at_ms = VALUES(updated_at_ms)`,
		config.AppCode,
		boolToInt(config.Enabled),
		config.CandidateRoomMaxOnline,
		config.RoomFullStopOnline,
		config.RoomTargetMinOnline,
		config.RoomTargetMaxOnline,
		config.RobotStayMinMS,
		config.RobotStayMaxMS,
		config.RobotReplaceMinMS,
		config.RobotReplaceMaxMS,
		config.NormalGiftIntervalMS,
		config.NormalGiftIntervalMinMS,
		config.NormalGiftIntervalMaxMS,
		string(rawGifts),
		string(rawLucky),
		string(rawSuperLucky),
		config.LuckyComboMin,
		config.LuckyComboMax,
		config.LuckyPauseMinMS,
		config.LuckyPauseMaxMS,
		config.MaxGiftSenders,
		string(rawPools),
		string(rawAllowedOwners),
		boolToInt(config.CountryLimitEnabled),
		string(rawCountryRules),
		config.UpdatedByAdminID,
		config.CreatedAtMS,
		config.UpdatedAtMS,
	)
	if err != nil {
		return roomservice.HumanRoomRobotConfig{}, err
	}
	saved, _, err := r.GetHumanRoomRobotConfig(ctx)
	return saved, err
}

// ListHumanRobotCandidateRooms 找出同国家低人数真人房；allowedOwnerIDs 为空时保持全量扫描，非空时只命中房主短号或内部 owner_user_id。
func (r *Repository) ListHumanRobotCandidateRooms(ctx context.Context, appCode string, countryCode string, maxOnline int32, fullStopOnline int32, limit int, allowedOwnerIDs []string) ([]roomservice.RoomListEntry, error) {
	appCode = normalizedRecordAppCode(ctx, appCode)
	countryCode = strings.ToUpper(strings.TrimSpace(countryCode))
	if maxOnline <= 0 {
		maxOnline = 5
	}
	if fullStopOnline <= 0 {
		fullStopOnline = 10
	}
	if maxOnline > fullStopOnline {
		maxOnline = fullStopOnline
	}
	if limit <= 0 || limit > 100 {
		limit = 50
	}
	shortIDs, ownerUserIDs := splitHumanRobotAllowedOwnerIDs(allowedOwnerIDs)
	where := []string{
		"app_code = ?",
		"status = 'active'",
		"owner_country_code = ?",
		"online_count < ?",
		"online_count < ?",
		"room_id NOT LIKE 'robot\\_%'",
	}
	args := []any{appCode, countryCode, maxOnline, fullStopOnline}
	if len(shortIDs) > 0 {
		ownerWhere := make([]string, 0, 2)
		shortPlaceholders := strings.TrimRight(strings.Repeat("?,", len(shortIDs)), ",")
		ownerWhere = append(ownerWhere, "room_short_id IN ("+shortPlaceholders+")")
		for _, id := range shortIDs {
			args = append(args, id)
		}
		if len(ownerUserIDs) > 0 {
			userPlaceholders := strings.TrimRight(strings.Repeat("?,", len(ownerUserIDs)), ",")
			ownerWhere = append(ownerWhere, "owner_user_id IN ("+userPlaceholders+")")
			for _, id := range ownerUserIDs {
				args = append(args, id)
			}
		}
		where = append(where, "("+strings.Join(ownerWhere, " OR ")+")")
	}
	args = append(args, limit)
	query := `SELECT app_code, room_id, room_short_id, visible_region_id, owner_country_code,
		        owner_user_id, title, cover_url, mode, status, locked, heat,
		        online_count, seat_count, occupied_seat_count, sort_score, created_at_ms, updated_at_ms
		   FROM room_list_entries
		  WHERE ` + strings.Join(where, "\n		    AND ") + `
		  ORDER BY online_count ASC, updated_at_ms DESC, room_id DESC
		  LIMIT ?`
	rows, err := r.db.QueryContext(ctx, query, args...)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	items := make([]roomservice.RoomListEntry, 0, limit)
	for rows.Next() {
		var entry roomservice.RoomListEntry
		if err := rows.Scan(
			&entry.AppCode,
			&entry.RoomID,
			&entry.RoomShortID,
			&entry.VisibleRegionID,
			&entry.OwnerCountryCode,
			&entry.OwnerUserID,
			&entry.Title,
			&entry.CoverURL,
			&entry.Mode,
			&entry.Status,
			&entry.Locked,
			&entry.Heat,
			&entry.OnlineCount,
			&entry.SeatCount,
			&entry.OccupiedSeatCount,
			&entry.SortScore,
			&entry.CreatedAtMS,
			&entry.UpdatedAtMS,
		); err != nil {
			return nil, err
		}
		items = append(items, entry)
	}
	return items, rows.Err()
}

type humanRoomRobotConfigScanner interface {
	Scan(dest ...any) error
}

func scanHumanRoomRobotConfig(row humanRoomRobotConfigScanner) (roomservice.HumanRoomRobotConfig, error) {
	var config roomservice.HumanRoomRobotConfig
	var enabled int
	var rawGifts string
	var rawLucky string
	var rawSuperLucky string
	var rawPools string
	var rawAllowedOwners string
	var countryLimitEnabled int
	var rawCountryRules string
	if err := row.Scan(
		&config.AppCode,
		&enabled,
		&config.CandidateRoomMaxOnline,
		&config.RoomFullStopOnline,
		&config.RoomTargetMinOnline,
		&config.RoomTargetMaxOnline,
		&config.RobotStayMinMS,
		&config.RobotStayMaxMS,
		&config.RobotReplaceMinMS,
		&config.RobotReplaceMaxMS,
		&config.NormalGiftIntervalMS,
		&config.NormalGiftIntervalMinMS,
		&config.NormalGiftIntervalMaxMS,
		&rawGifts,
		&rawLucky,
		&rawSuperLucky,
		&config.LuckyComboMin,
		&config.LuckyComboMax,
		&config.LuckyPauseMinMS,
		&config.LuckyPauseMaxMS,
		&config.MaxGiftSenders,
		&rawPools,
		&rawAllowedOwners,
		&countryLimitEnabled,
		&rawCountryRules,
		&config.UpdatedByAdminID,
		&config.CreatedAtMS,
		&config.UpdatedAtMS,
	); err != nil {
		return roomservice.HumanRoomRobotConfig{}, err
	}
	config.Enabled = enabled == 1
	if err := json.Unmarshal([]byte(rawGifts), &config.GiftIDs); err != nil {
		return roomservice.HumanRoomRobotConfig{}, err
	}
	if err := json.Unmarshal([]byte(rawLucky), &config.LuckyGiftIDs); err != nil {
		return roomservice.HumanRoomRobotConfig{}, err
	}
	if err := json.Unmarshal([]byte(rawSuperLucky), &config.SuperLuckyGiftIDs); err != nil {
		return roomservice.HumanRoomRobotConfig{}, err
	}
	if err := json.Unmarshal([]byte(rawPools), &config.CountryPools); err != nil {
		return roomservice.HumanRoomRobotConfig{}, err
	}
	if err := json.Unmarshal([]byte(rawAllowedOwners), &config.AllowedOwnerIDs); err != nil {
		return roomservice.HumanRoomRobotConfig{}, err
	}
	config.CountryLimitEnabled = countryLimitEnabled == 1
	if err := json.Unmarshal([]byte(rawCountryRules), &config.CountryRules); err != nil {
		return roomservice.HumanRoomRobotConfig{}, err
	}
	return config, nil
}

func humanRoomRobotConfigJSON(config roomservice.HumanRoomRobotConfig) ([]byte, []byte, []byte, []byte, []byte, []byte, error) {
	rawGifts, err := json.Marshal(config.GiftIDs)
	if err != nil {
		return nil, nil, nil, nil, nil, nil, err
	}
	rawLucky, err := json.Marshal(config.LuckyGiftIDs)
	if err != nil {
		return nil, nil, nil, nil, nil, nil, err
	}
	rawSuperLucky, err := json.Marshal(config.SuperLuckyGiftIDs)
	if err != nil {
		return nil, nil, nil, nil, nil, nil, err
	}
	rawPools, err := json.Marshal(config.CountryPools)
	if err != nil {
		return nil, nil, nil, nil, nil, nil, err
	}
	rawAllowedOwners, err := json.Marshal(config.AllowedOwnerIDs)
	if err != nil {
		return nil, nil, nil, nil, nil, nil, err
	}
	rawCountryRules, err := json.Marshal(config.CountryRules)
	if err != nil {
		return nil, nil, nil, nil, nil, nil, err
	}
	return rawGifts, rawLucky, rawSuperLucky, rawPools, rawAllowedOwners, rawCountryRules, nil
}

func splitHumanRobotAllowedOwnerIDs(values []string) ([]string, []int64) {
	seenShortIDs := make(map[string]bool, len(values))
	seenUserIDs := make(map[int64]bool, len(values))
	shortIDs := make([]string, 0, len(values))
	userIDs := make([]int64, 0, len(values))
	for _, value := range values {
		value = strings.TrimSpace(value)
		if value == "" || seenShortIDs[value] {
			continue
		}
		seenShortIDs[value] = true
		shortIDs = append(shortIDs, value)
		if strings.ContainsAny(value, ".eE") {
			continue
		}
		userID, err := strconv.ParseInt(value, 10, 64)
		if err != nil || userID <= 0 || seenUserIDs[userID] {
			continue
		}
		seenUserIDs[userID] = true
		userIDs = append(userIDs, userID)
	}
	return shortIDs, userIDs
}

type robotRoomScanner interface {
	Scan(dest ...any) error
}

func scanRobotRoomConfigs(rows *sql.Rows) ([]roomservice.RobotRoomConfig, error) {
	items := make([]roomservice.RobotRoomConfig, 0)
	for rows.Next() {
		item, err := scanRobotRoomConfig(rows)
		if err != nil {
			return nil, err
		}
		items = append(items, item)
	}
	return items, rows.Err()
}

func scanRobotRoomConfig(row robotRoomScanner) (roomservice.RobotRoomConfig, error) {
	var item roomservice.RobotRoomConfig
	var rawRobots string
	var rawGifts string
	var rawLuckyGifts string
	if err := row.Scan(
		&item.AppCode,
		&item.RoomID,
		&item.RoomShortID,
		&item.Title,
		&item.CoverURL,
		&item.VisibleRegionID,
		&item.Status,
		&item.OwnerRobotUserID,
		&rawRobots,
		&item.ActiveRobotCount,
		&item.SeatCount,
		&rawGifts,
		&rawLuckyGifts,
		&item.GiftRule.NormalGiftIntervalMS,
		&item.GiftRule.LuckyComboMin,
		&item.GiftRule.LuckyComboMax,
		&item.GiftRule.LuckyPauseMinMS,
		&item.GiftRule.LuckyPauseMaxMS,
		&item.GiftRule.RobotStayMinMS,
		&item.GiftRule.RobotStayMaxMS,
		&item.GiftRule.RobotReplaceMinMS,
		&item.GiftRule.RobotReplaceMaxMS,
		&item.GiftRule.MaxGiftSenders,
		&item.CreatedByAdminID,
		&item.CreatedAtMS,
		&item.UpdatedAtMS,
	); err != nil {
		return roomservice.RobotRoomConfig{}, err
	}
	if err := json.Unmarshal([]byte(rawRobots), &item.RobotUserIDs); err != nil {
		return roomservice.RobotRoomConfig{}, err
	}
	if err := json.Unmarshal([]byte(rawGifts), &item.GiftRule.GiftIDs); err != nil {
		return roomservice.RobotRoomConfig{}, err
	}
	if err := json.Unmarshal([]byte(rawLuckyGifts), &item.GiftRule.LuckyGiftIDs); err != nil {
		return roomservice.RobotRoomConfig{}, err
	}
	if item.ActiveRobotCount <= 0 {
		item.ActiveRobotCount = int32(len(item.RobotUserIDs))
	}
	if item.SeatCount <= 0 {
		item.SeatCount = 10
	}
	item.GiftRule = roomservice.WithRobotRoomGiftRuleStorageDefaults(item.GiftRule)
	return item, nil
}

func robotRoomJSON(config roomservice.RobotRoomConfig) ([]byte, []byte, []byte, error) {
	rawRobots, err := json.Marshal(config.RobotUserIDs)
	if err != nil {
		return nil, nil, nil, err
	}
	rawGifts, err := json.Marshal(config.GiftRule.GiftIDs)
	if err != nil {
		return nil, nil, nil, err
	}
	rawLuckyGifts, err := json.Marshal(config.GiftRule.LuckyGiftIDs)
	if err != nil {
		return nil, nil, nil, err
	}
	return rawRobots, rawGifts, rawLuckyGifts, nil
}

func occupiedRobotUserIDsTx(ctx context.Context, tx *sql.Tx, appCode string, userIDs []int64) (map[int64]bool, error) {
	return occupiedRobotUserIDsQuery(ctx, tx, appCode, userIDs, true)
}

type dbQuerier interface {
	QueryContext(ctx context.Context, query string, args ...any) (*sql.Rows, error)
}

func occupiedRobotUserIDsQuery(ctx context.Context, db dbQuerier, appCode string, userIDs []int64, lock bool) (map[int64]bool, error) {
	ids := uniquePositiveInt64(userIDs)
	occupied := make(map[int64]bool, len(ids))
	if len(ids) == 0 {
		return occupied, nil
	}
	query := `SELECT robot_user_ids_json FROM room_robot_rooms WHERE app_code = ? AND status = 'active'`
	if lock {
		query += ` FOR UPDATE`
	}
	rows, err := db.QueryContext(ctx, query, appcode.Normalize(appCode))
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	wanted := make(map[int64]bool, len(ids))
	for _, id := range ids {
		wanted[id] = true
	}
	for rows.Next() {
		var raw string
		if err := rows.Scan(&raw); err != nil {
			return nil, err
		}
		var roomRobotIDs []int64
		if err := json.Unmarshal([]byte(raw), &roomRobotIDs); err != nil {
			return nil, err
		}
		for _, id := range roomRobotIDs {
			if wanted[id] {
				occupied[id] = true
			}
		}
	}
	return occupied, rows.Err()
}
