package mysql

import (
	"context"
	"database/sql"
	"errors"
	"fmt"
	"regexp"
	"strings"
	"time"

	_ "github.com/go-sql-driver/mysql"
	"hyapp/pkg/appcode"
	"hyapp/pkg/xerr"
	robotdomain "hyapp/services/robot-service/internal/domain/robot"
)

const (
	tableGameRobots        = "robot_game_robots"
	tableRoomRobots        = "robot_room_robots"
	tableServiceMigrations = "robot_service_migrations"
	migrationGameRobotsV1  = "game_robots_from_game_self_game_robots_v1"
	legacyGameRobotTable   = "game_self_game_robots"
	defaultLegacyGameID    = robotdomain.DefaultGameID
)

var mysqlIdentifierPattern = regexp.MustCompile(`^[A-Za-z0-9_]+$`)

// Repository 是 robot-service 的持久化入口；新逻辑只读写 hyapp_robot 下的 robot_* 表，旧 game_self_game_robots 只作为一次性迁移来源。
type Repository struct {
	db *sql.DB
}

func Open(ctx context.Context, dsn string) (*Repository, error) {
	db, err := sql.Open("mysql", strings.TrimSpace(dsn))
	if err != nil {
		return nil, err
	}
	db.SetMaxOpenConns(20)
	db.SetMaxIdleConns(10)
	db.SetConnMaxLifetime(30 * time.Minute)
	repo := &Repository{db: db}
	if err := repo.Migrate(ctx); err != nil {
		_ = db.Close()
		return nil, err
	}
	return repo, nil
}

func (r *Repository) Close() error {
	if r == nil || r.db == nil {
		return nil
	}
	return r.db.Close()
}

func (r *Repository) Ping(ctx context.Context) error {
	if r == nil || r.db == nil {
		return xerr.New(xerr.Unavailable, "robot repository is not configured")
	}
	return r.db.PingContext(ctx)
}

func (r *Repository) Migrate(ctx context.Context) error {
	statements := []string{
		`CREATE TABLE IF NOT EXISTS robot_game_robots (
		  app_code VARCHAR(32) NOT NULL,
		  game_id VARCHAR(96) NOT NULL,
		  user_id BIGINT NOT NULL,
		  status VARCHAR(32) NOT NULL DEFAULT 'active',
		  created_by_admin_id BIGINT NOT NULL DEFAULT 0,
		  created_at_ms BIGINT NOT NULL,
		  updated_at_ms BIGINT NOT NULL,
		  last_used_at_ms BIGINT NOT NULL DEFAULT 0,
		  used_count BIGINT NOT NULL DEFAULT 0,
		  PRIMARY KEY(app_code, game_id, user_id),
		  KEY idx_robot_game_status(app_code, game_id, status, updated_at_ms, user_id),
		  KEY idx_robot_game_random_pool(app_code, status, user_id)
		) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`,
		`CREATE TABLE IF NOT EXISTS robot_room_robots (
		  app_code VARCHAR(32) NOT NULL,
		  room_scene VARCHAR(64) NOT NULL,
		  user_id BIGINT NOT NULL,
		  status VARCHAR(32) NOT NULL DEFAULT 'active',
		  created_by_admin_id BIGINT NOT NULL DEFAULT 0,
		  created_at_ms BIGINT NOT NULL,
		  updated_at_ms BIGINT NOT NULL,
		  last_used_at_ms BIGINT NOT NULL DEFAULT 0,
		  used_count BIGINT NOT NULL DEFAULT 0,
		  PRIMARY KEY(app_code, room_scene, user_id),
		  KEY idx_robot_room_status(app_code, room_scene, status, updated_at_ms, user_id),
		  KEY idx_robot_room_random_pool(app_code, status, user_id)
		) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`,
		`CREATE TABLE IF NOT EXISTS robot_service_migrations (
		  migration_key VARCHAR(128) NOT NULL,
		  source_database VARCHAR(64) NOT NULL DEFAULT '',
		  source_table VARCHAR(64) NOT NULL DEFAULT '',
		  migrated_rows BIGINT NOT NULL DEFAULT 0,
		  status VARCHAR(32) NOT NULL,
		  created_at_ms BIGINT NOT NULL,
		  updated_at_ms BIGINT NOT NULL,
		  PRIMARY KEY(migration_key)
		) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`,
	}
	for _, statement := range statements {
		if _, err := r.db.ExecContext(ctx, statement); err != nil {
			return err
		}
	}
	return nil
}

type MigrationResult struct {
	Applied          bool
	AlreadyCompleted bool
	SourceFound      bool
	LegacyDatabase   string
	MigratedRows     int64
}

func (r *Repository) MigrateLegacyGameRobots(ctx context.Context, legacyDatabase string) (MigrationResult, error) {
	legacyDatabase = strings.TrimSpace(legacyDatabase)
	result := MigrationResult{LegacyDatabase: legacyDatabase}
	if legacyDatabase == "" {
		return result, nil
	}
	if !mysqlIdentifierPattern.MatchString(legacyDatabase) {
		return result, xerr.New(xerr.InvalidArgument, "legacy game database name is invalid")
	}
	completed, err := r.migrationCompleted(ctx, migrationGameRobotsV1)
	if err != nil {
		return result, err
	}
	if completed {
		result.AlreadyCompleted = true
		return result, nil
	}
	sourceFound, err := r.tableExistsInSchema(ctx, legacyDatabase, legacyGameRobotTable)
	if err != nil {
		return result, err
	}
	result.SourceFound = sourceFound
	if !sourceFound {
		return result, nil
	}

	tx, err := r.db.BeginTx(ctx, nil)
	if err != nil {
		return result, err
	}
	defer func() { _ = tx.Rollback() }()

	// 用迁移表行锁串行化首次复制，避免多实例启动时重复读旧表；复制只插入/补齐新表，绝不删除旧 game-service 数据。
	var lockedKey string
	lockErr := tx.QueryRowContext(ctx, `SELECT migration_key FROM robot_service_migrations WHERE migration_key = ? FOR UPDATE`, migrationGameRobotsV1).Scan(&lockedKey)
	if lockErr != nil && !errors.Is(lockErr, sql.ErrNoRows) {
		return result, lockErr
	}
	if lockedKey != "" {
		if err := tx.Commit(); err != nil {
			return result, err
		}
		result.AlreadyCompleted = true
		return result, nil
	}

	sourceTable := quoteQualifiedIdentifier(legacyDatabase, legacyGameRobotTable)
	var sourceRows int64
	countQuery := fmt.Sprintf(`SELECT COUNT(*) FROM (SELECT app_code, user_id FROM %s GROUP BY app_code, user_id) migrated`, sourceTable)
	if err := tx.QueryRowContext(ctx, countQuery).Scan(&sourceRows); err != nil {
		return result, err
	}
	copyQuery := fmt.Sprintf(
		`INSERT INTO robot_game_robots (
		   app_code, game_id, user_id, status, created_by_admin_id, created_at_ms, updated_at_ms, last_used_at_ms, used_count
		 )
		 SELECT app_code, ? AS game_id, user_id,
		        IF(SUM(status = ?) > 0, ?, ?) AS status,
		        MIN(created_by_admin_id) AS created_by_admin_id,
		        MIN(created_at_ms) AS created_at_ms,
		        MAX(updated_at_ms) AS updated_at_ms,
		        MAX(last_used_at_ms) AS last_used_at_ms,
		        MAX(used_count) AS used_count
		   FROM %s
		  GROUP BY app_code, user_id
		 ON DUPLICATE KEY UPDATE
		   status = IF(VALUES(status) = ?, ?, robot_game_robots.status),
		   updated_at_ms = GREATEST(robot_game_robots.updated_at_ms, VALUES(updated_at_ms)),
		   last_used_at_ms = GREATEST(robot_game_robots.last_used_at_ms, VALUES(last_used_at_ms)),
		   used_count = GREATEST(robot_game_robots.used_count, VALUES(used_count))`,
		sourceTable,
	)
	if _, err := tx.ExecContext(ctx, copyQuery,
		defaultLegacyGameID,
		robotdomain.StatusActive,
		robotdomain.StatusActive,
		robotdomain.StatusDisabled,
		robotdomain.StatusActive,
		robotdomain.StatusActive,
	); err != nil {
		return result, err
	}
	nowMS := time.Now().UnixMilli()
	if _, err := tx.ExecContext(ctx,
		`INSERT INTO robot_service_migrations (
		   migration_key, source_database, source_table, migrated_rows, status, created_at_ms, updated_at_ms
		 ) VALUES (?, ?, ?, ?, ?, ?, ?)
		 ON DUPLICATE KEY UPDATE updated_at_ms = VALUES(updated_at_ms)`,
		migrationGameRobotsV1, legacyDatabase, legacyGameRobotTable, sourceRows, "completed", nowMS, nowMS,
	); err != nil {
		return result, err
	}
	if err := tx.Commit(); err != nil {
		return result, err
	}
	result.Applied = true
	result.MigratedRows = sourceRows
	return result, nil
}

func (r *Repository) PickGameRobot(ctx context.Context, appCode string, gameID string, excludeUserIDs []int64, nowMS int64) (robotdomain.Robot, error) {
	app := appcode.Normalize(appCode)
	gameID = robotdomain.NormalizeGameID(gameID)
	tx, err := r.db.BeginTx(ctx, nil)
	if err != nil {
		return robotdomain.Robot{}, err
	}
	defer func() { _ = tx.Rollback() }()

	var item robotdomain.Robot
	err = tx.QueryRowContext(ctx,
		gameRobotPickQuery(excludeUserIDs),
		append([]any{
			gameID, robotdomain.StatusActive, app, robotdomain.StatusActive,
		}, int64SliceToAny(uniquePositiveInt64(excludeUserIDs))...)...,
	).Scan(
		&item.AppCode, &item.GameID, &item.UserID, &item.Status, &item.CreatedByAdminID,
		&item.CreatedAtMS, &item.UpdatedAtMS, &item.LastUsedAtMS, &item.UsedCount,
	)
	if err != nil {
		if errors.Is(err, sql.ErrNoRows) {
			return robotdomain.Robot{}, xerr.New(xerr.NotFound, "game robot not found")
		}
		return robotdomain.Robot{}, err
	}
	res, err := tx.ExecContext(ctx,
		`UPDATE robot_game_robots
		    SET last_used_at_ms = ?, used_count = used_count + 1
		  WHERE app_code = ? AND user_id = ? AND status = ?`,
		nowMS, app, item.UserID, robotdomain.StatusActive,
	)
	if err != nil {
		return robotdomain.Robot{}, err
	}
	affected, _ := res.RowsAffected()
	if affected == 0 {
		return robotdomain.Robot{}, xerr.New(xerr.NotFound, "game robot not found")
	}
	if err := tx.Commit(); err != nil {
		return robotdomain.Robot{}, err
	}
	item.Scope = robotdomain.ScopeGame
	item.LastUsedAtMS = nowMS
	item.UsedCount++
	return item, nil
}

func gameRobotPickQuery(excludeUserIDs []int64) string {
	query := `SELECT r.app_code, ? AS game_id, r.user_id, ? AS status,
		        r.created_by_admin_id, r.created_at_ms, r.updated_at_ms,
		        r.last_used_at_ms, r.used_count
		   FROM robot_game_robots r
		  WHERE r.app_code = ? AND r.status = ?`
	if ids := uniquePositiveInt64(excludeUserIDs); len(ids) > 0 {
		query += ` AND r.user_id NOT IN (` + strings.TrimRight(strings.Repeat("?,", len(ids)), ",") + `)`
	}
	query += ` ORDER BY RAND() LIMIT 1 FOR UPDATE SKIP LOCKED`
	return query
}

func (r *Repository) ListGameRobots(ctx context.Context, appCode string, gameID string, status string, pageSize int32, cursor string) ([]robotdomain.Robot, string, error) {
	app := appcode.Normalize(appCode)
	gameID = robotdomain.NormalizeGameID(gameID)
	if pageSize <= 0 || pageSize > 200 {
		pageSize = 50
	}
	status = strings.TrimSpace(status)
	cursorID := int64(0)
	if strings.TrimSpace(cursor) != "" {
		_, _ = fmt.Sscan(strings.TrimSpace(cursor), &cursorID)
	}
	args := []any{gameID, robotdomain.StatusActive, robotdomain.StatusActive, robotdomain.StatusDisabled, app}
	query := `SELECT app_code, game_id, user_id, status, created_by_admin_id, created_at_ms, updated_at_ms, last_used_at_ms, used_count
	            FROM (
	              SELECT app_code, ? AS game_id, user_id,
	                     IF(SUM(status = ?) > 0, ?, ?) AS status,
	                     MIN(created_by_admin_id) AS created_by_admin_id,
	                     MIN(created_at_ms) AS created_at_ms,
	                     MAX(updated_at_ms) AS updated_at_ms,
	                     MAX(last_used_at_ms) AS last_used_at_ms,
	                     MAX(used_count) AS used_count
	                FROM robot_game_robots
	               WHERE app_code = ?`
	if cursorID > 0 {
		query += ` AND user_id > ?`
		args = append(args, cursorID)
	}
	query += ` GROUP BY app_code, user_id
	            ) robots
	           WHERE (? = '' OR status = ?)
	           ORDER BY user_id ASC LIMIT ?`
	args = append(args, status, status, int(pageSize)+1)
	return scanRobotList(ctx, r.db, robotdomain.ScopeGame, query, args, pageSize)
}

func (r *Repository) RegisterGameRobots(ctx context.Context, appCode string, gameID string, userIDs []int64, adminID int64, nowMS int64) ([]robotdomain.Robot, error) {
	app := appcode.Normalize(appCode)
	gameID = robotdomain.DefaultGameID
	tx, err := r.db.BeginTx(ctx, nil)
	if err != nil {
		return nil, err
	}
	defer func() { _ = tx.Rollback() }()
	for _, userID := range uniquePositiveInt64(userIDs) {
		if _, err := tx.ExecContext(ctx,
			`INSERT INTO robot_game_robots (
			   app_code, game_id, user_id, status, created_by_admin_id, created_at_ms, updated_at_ms,
			   last_used_at_ms, used_count
			 ) VALUES (?, ?, ?, ?, ?, ?, ?, 0, 0)
			 ON DUPLICATE KEY UPDATE status = VALUES(status), updated_at_ms = VALUES(updated_at_ms)`,
			app, gameID, userID, robotdomain.StatusActive, adminID, nowMS, nowMS,
		); err != nil {
			return nil, err
		}
	}
	if err := tx.Commit(); err != nil {
		return nil, err
	}
	items, _, err := r.ListGameRobots(ctx, app, "", "", 200, "")
	return items, err
}

func (r *Repository) SetGameRobotStatus(ctx context.Context, appCode string, gameID string, userID int64, status string, nowMS int64) (robotdomain.Robot, error) {
	app := appcode.Normalize(appCode)
	gameID = robotdomain.NormalizeGameID(gameID)
	status = robotdomain.NormalizeStatus(status)
	if status != robotdomain.StatusActive && status != robotdomain.StatusDisabled {
		return robotdomain.Robot{}, xerr.New(xerr.InvalidArgument, "game robot status is invalid")
	}
	res, err := r.db.ExecContext(ctx,
		`UPDATE robot_game_robots
		    SET status = ?, updated_at_ms = ?
		  WHERE app_code = ? AND user_id = ?`,
		status, nowMS, app, userID,
	)
	if err != nil {
		return robotdomain.Robot{}, err
	}
	affected, _ := res.RowsAffected()
	if affected == 0 {
		return robotdomain.Robot{}, xerr.New(xerr.NotFound, "game robot not found")
	}
	return r.getUniversalGameRobot(ctx, app, gameID, userID)
}

func (r *Repository) DeleteGameRobot(ctx context.Context, appCode string, userID int64) error {
	app := appcode.Normalize(appCode)
	res, err := r.db.ExecContext(ctx, `DELETE FROM robot_game_robots WHERE app_code = ? AND user_id = ?`, app, userID)
	if err != nil {
		return err
	}
	affected, _ := res.RowsAffected()
	if affected == 0 {
		return xerr.New(xerr.NotFound, "game robot not found")
	}
	return nil
}

func (r *Repository) getUniversalGameRobot(ctx context.Context, appCode string, gameID string, userID int64) (robotdomain.Robot, error) {
	var item robotdomain.Robot
	err := r.db.QueryRowContext(ctx,
		`SELECT app_code, ? AS game_id, user_id,
		        IF(SUM(status = ?) > 0, ?, ?) AS status,
		        MIN(created_by_admin_id), MIN(created_at_ms), MAX(updated_at_ms),
		        MAX(last_used_at_ms), MAX(used_count)
		   FROM robot_game_robots
		  WHERE app_code = ? AND user_id = ?
		  GROUP BY app_code, user_id`,
		gameID, robotdomain.StatusActive, robotdomain.StatusActive, robotdomain.StatusDisabled, appCode, userID,
	).Scan(
		&item.AppCode, &item.GameID, &item.UserID, &item.Status, &item.CreatedByAdminID,
		&item.CreatedAtMS, &item.UpdatedAtMS, &item.LastUsedAtMS, &item.UsedCount,
	)
	if err != nil {
		if errors.Is(err, sql.ErrNoRows) {
			return robotdomain.Robot{}, xerr.New(xerr.NotFound, "game robot not found")
		}
		return robotdomain.Robot{}, err
	}
	item.Scope = robotdomain.ScopeGame
	return item, nil
}

func (r *Repository) PickRoomRobot(ctx context.Context, appCode string, roomScene string, roomID string, nowMS int64) (robotdomain.Robot, error) {
	app := appcode.Normalize(appCode)
	roomScene = robotdomain.NormalizeRoomScene(roomScene)
	tx, err := r.db.BeginTx(ctx, nil)
	if err != nil {
		return robotdomain.Robot{}, err
	}
	defer func() { _ = tx.Rollback() }()
	var item robotdomain.Robot
	err = tx.QueryRowContext(ctx,
		`SELECT app_code, room_scene, user_id, status, created_by_admin_id, created_at_ms, updated_at_ms, last_used_at_ms, used_count
		   FROM robot_room_robots
		  WHERE app_code = ? AND room_scene = ? AND status = ?
		  ORDER BY RAND()
		  LIMIT 1
		  FOR UPDATE SKIP LOCKED`,
		app, roomScene, robotdomain.StatusActive,
	).Scan(&item.AppCode, &item.RoomScene, &item.UserID, &item.Status, &item.CreatedByAdminID, &item.CreatedAtMS, &item.UpdatedAtMS, &item.LastUsedAtMS, &item.UsedCount)
	if err != nil {
		if errors.Is(err, sql.ErrNoRows) {
			return robotdomain.Robot{}, xerr.New(xerr.NotFound, "room robot not found")
		}
		return robotdomain.Robot{}, err
	}
	res, err := tx.ExecContext(ctx,
		`UPDATE robot_room_robots SET last_used_at_ms = ?, used_count = used_count + 1 WHERE app_code = ? AND room_scene = ? AND user_id = ? AND status = ?`,
		nowMS, app, roomScene, item.UserID, robotdomain.StatusActive,
	)
	if err != nil {
		return robotdomain.Robot{}, err
	}
	affected, _ := res.RowsAffected()
	if affected == 0 {
		return robotdomain.Robot{}, xerr.New(xerr.NotFound, "room robot not found")
	}
	if err := tx.Commit(); err != nil {
		return robotdomain.Robot{}, err
	}
	item.Scope = robotdomain.ScopeRoom
	item.LastUsedAtMS = nowMS
	item.UsedCount++
	return item, nil
}

func (r *Repository) ListRoomRobots(ctx context.Context, appCode string, roomScene string, status string, pageSize int32, cursor string) ([]robotdomain.Robot, string, error) {
	app := appcode.Normalize(appCode)
	roomScene = robotdomain.NormalizeRoomScene(roomScene)
	if pageSize <= 0 || pageSize > 200 {
		pageSize = 50
	}
	status = strings.TrimSpace(status)
	cursorID := int64(0)
	if strings.TrimSpace(cursor) != "" {
		_, _ = fmt.Sscan(strings.TrimSpace(cursor), &cursorID)
	}
	args := []any{app, roomScene}
	query := `SELECT app_code, room_scene, user_id, status, created_by_admin_id, created_at_ms, updated_at_ms, last_used_at_ms, used_count
	            FROM robot_room_robots
	           WHERE app_code = ? AND room_scene = ?`
	if cursorID > 0 {
		query += ` AND user_id > ?`
		args = append(args, cursorID)
	}
	query += ` AND (? = '' OR status = ?) ORDER BY user_id ASC LIMIT ?`
	args = append(args, status, status, int(pageSize)+1)
	return scanRobotList(ctx, r.db, robotdomain.ScopeRoom, query, args, pageSize)
}

func (r *Repository) RegisterRoomRobots(ctx context.Context, appCode string, roomScene string, userIDs []int64, adminID int64, nowMS int64) ([]robotdomain.Robot, error) {
	app := appcode.Normalize(appCode)
	roomScene = robotdomain.NormalizeRoomScene(roomScene)
	tx, err := r.db.BeginTx(ctx, nil)
	if err != nil {
		return nil, err
	}
	defer func() { _ = tx.Rollback() }()
	for _, userID := range uniquePositiveInt64(userIDs) {
		if _, err := tx.ExecContext(ctx,
			`INSERT INTO robot_room_robots (
			   app_code, room_scene, user_id, status, created_by_admin_id, created_at_ms, updated_at_ms, last_used_at_ms, used_count
			 ) VALUES (?, ?, ?, ?, ?, ?, ?, 0, 0)
			 ON DUPLICATE KEY UPDATE status = VALUES(status), updated_at_ms = VALUES(updated_at_ms)`,
			app, roomScene, userID, robotdomain.StatusActive, adminID, nowMS, nowMS,
		); err != nil {
			return nil, err
		}
	}
	if err := tx.Commit(); err != nil {
		return nil, err
	}
	items, _, err := r.ListRoomRobots(ctx, app, roomScene, "", 200, "")
	return items, err
}

func (r *Repository) SetRoomRobotStatus(ctx context.Context, appCode string, roomScene string, userID int64, status string, nowMS int64) (robotdomain.Robot, error) {
	app := appcode.Normalize(appCode)
	roomScene = robotdomain.NormalizeRoomScene(roomScene)
	status = robotdomain.NormalizeStatus(status)
	if status != robotdomain.StatusActive && status != robotdomain.StatusDisabled {
		return robotdomain.Robot{}, xerr.New(xerr.InvalidArgument, "room robot status is invalid")
	}
	res, err := r.db.ExecContext(ctx,
		`UPDATE robot_room_robots SET status = ?, updated_at_ms = ? WHERE app_code = ? AND room_scene = ? AND user_id = ?`,
		status, nowMS, app, roomScene, userID,
	)
	if err != nil {
		return robotdomain.Robot{}, err
	}
	affected, _ := res.RowsAffected()
	if affected == 0 {
		return robotdomain.Robot{}, xerr.New(xerr.NotFound, "room robot not found")
	}
	return r.getRoomRobot(ctx, app, roomScene, userID)
}

func (r *Repository) DeleteRoomRobot(ctx context.Context, appCode string, roomScene string, userID int64) error {
	app := appcode.Normalize(appCode)
	roomScene = robotdomain.NormalizeRoomScene(roomScene)
	res, err := r.db.ExecContext(ctx, `DELETE FROM robot_room_robots WHERE app_code = ? AND room_scene = ? AND user_id = ?`, app, roomScene, userID)
	if err != nil {
		return err
	}
	affected, _ := res.RowsAffected()
	if affected == 0 {
		return xerr.New(xerr.NotFound, "room robot not found")
	}
	return nil
}

func (r *Repository) getRoomRobot(ctx context.Context, appCode string, roomScene string, userID int64) (robotdomain.Robot, error) {
	var item robotdomain.Robot
	err := r.db.QueryRowContext(ctx,
		`SELECT app_code, room_scene, user_id, status, created_by_admin_id, created_at_ms, updated_at_ms, last_used_at_ms, used_count
		   FROM robot_room_robots WHERE app_code = ? AND room_scene = ? AND user_id = ?`,
		appCode, roomScene, userID,
	).Scan(&item.AppCode, &item.RoomScene, &item.UserID, &item.Status, &item.CreatedByAdminID, &item.CreatedAtMS, &item.UpdatedAtMS, &item.LastUsedAtMS, &item.UsedCount)
	if err != nil {
		if errors.Is(err, sql.ErrNoRows) {
			return robotdomain.Robot{}, xerr.New(xerr.NotFound, "room robot not found")
		}
		return robotdomain.Robot{}, err
	}
	item.Scope = robotdomain.ScopeRoom
	return item, nil
}

func (r *Repository) dropIndexIfExists(ctx context.Context, tableName string, indexName string) error {
	var count int
	if err := r.db.QueryRowContext(ctx,
		`SELECT COUNT(*)
		   FROM information_schema.STATISTICS
		  WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? AND INDEX_NAME = ?`,
		tableName, indexName,
	).Scan(&count); err != nil {
		return err
	}
	if count == 0 {
		return nil
	}
	_, err := r.db.ExecContext(ctx, `ALTER TABLE `+tableName+` DROP INDEX `+indexName)
	return err
}

func (r *Repository) ensureColumn(ctx context.Context, tableName string, columnName string, columnDDL string) error {
	var count int
	if err := r.db.QueryRowContext(ctx,
		`SELECT COUNT(*)
		   FROM information_schema.COLUMNS
		  WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? AND COLUMN_NAME = ?`,
		tableName, columnName,
	).Scan(&count); err != nil {
		return err
	}
	if count > 0 {
		return nil
	}
	_, err := r.db.ExecContext(ctx, `ALTER TABLE `+tableName+` ADD COLUMN `+columnDDL)
	return err
}

func (r *Repository) ensureIndexDefinition(ctx context.Context, tableName string, indexName string, columns []string, createDDL string) error {
	rows, err := r.db.QueryContext(ctx,
		`SELECT COLUMN_NAME
		   FROM information_schema.STATISTICS
		  WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? AND INDEX_NAME = ?
		  ORDER BY SEQ_IN_INDEX`,
		tableName, indexName,
	)
	if err != nil {
		return err
	}
	defer rows.Close()
	existing := make([]string, 0, len(columns))
	for rows.Next() {
		var column string
		if err := rows.Scan(&column); err != nil {
			return err
		}
		existing = append(existing, column)
	}
	if err := rows.Err(); err != nil {
		return err
	}
	if equalStringSlices(existing, columns) {
		return nil
	}
	if len(existing) > 0 {
		if _, err := r.db.ExecContext(ctx, `ALTER TABLE `+tableName+` DROP INDEX `+indexName); err != nil {
			return err
		}
	}
	_, err = r.db.ExecContext(ctx, createDDL)
	return err
}

func equalStringSlices(left []string, right []string) bool {
	if len(left) != len(right) {
		return false
	}
	for i := range left {
		if left[i] != right[i] {
			return false
		}
	}
	return true
}

func (r *Repository) migrationCompleted(ctx context.Context, key string) (bool, error) {
	var count int
	if err := r.db.QueryRowContext(ctx,
		`SELECT COUNT(*) FROM robot_service_migrations WHERE migration_key = ? AND status = ?`,
		key, "completed",
	).Scan(&count); err != nil {
		return false, err
	}
	return count > 0, nil
}

func (r *Repository) tableExistsInSchema(ctx context.Context, schemaName string, tableName string) (bool, error) {
	var count int
	if err := r.db.QueryRowContext(ctx,
		`SELECT COUNT(*)
		   FROM information_schema.TABLES
		  WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ?`,
		schemaName, tableName,
	).Scan(&count); err != nil {
		return false, err
	}
	return count > 0, nil
}

func quoteQualifiedIdentifier(schemaName string, tableName string) string {
	return quoteIdentifier(schemaName) + "." + quoteIdentifier(tableName)
}

func quoteIdentifier(identifier string) string {
	return "`" + strings.ReplaceAll(identifier, "`", "``") + "`"
}

type queryer interface {
	QueryContext(context.Context, string, ...any) (*sql.Rows, error)
}

func scanRobotList(ctx context.Context, db queryer, scope string, query string, args []any, pageSize int32) ([]robotdomain.Robot, string, error) {
	rows, err := db.QueryContext(ctx, query, args...)
	if err != nil {
		return nil, "", err
	}
	defer rows.Close()
	items := []robotdomain.Robot{}
	for rows.Next() {
		var item robotdomain.Robot
		if scope == robotdomain.ScopeRoom {
			if err := rows.Scan(&item.AppCode, &item.RoomScene, &item.UserID, &item.Status, &item.CreatedByAdminID, &item.CreatedAtMS, &item.UpdatedAtMS, &item.LastUsedAtMS, &item.UsedCount); err != nil {
				return nil, "", err
			}
		} else {
			if err := rows.Scan(&item.AppCode, &item.GameID, &item.UserID, &item.Status, &item.CreatedByAdminID, &item.CreatedAtMS, &item.UpdatedAtMS, &item.LastUsedAtMS, &item.UsedCount); err != nil {
				return nil, "", err
			}
		}
		item.Scope = scope
		items = append(items, item)
	}
	if err := rows.Err(); err != nil {
		return nil, "", err
	}
	nextCursor := ""
	if len(items) > int(pageSize) {
		nextCursor = fmt.Sprintf("%d", items[pageSize-1].UserID)
		items = items[:pageSize]
	}
	return items, nextCursor, nil
}

func uniquePositiveInt64(values []int64) []int64 {
	seen := map[int64]struct{}{}
	out := make([]int64, 0, len(values))
	for _, value := range values {
		if value <= 0 {
			continue
		}
		if _, ok := seen[value]; ok {
			continue
		}
		seen[value] = struct{}{}
		out = append(out, value)
	}
	return out
}

func int64SliceToAny(values []int64) []any {
	out := make([]any, 0, len(values))
	for _, value := range values {
		out = append(out, value)
	}
	return out
}
