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

import "context"

// Migrate 创建 room-service 首版需要的 MySQL 表。
func (r *Repository) Migrate(ctx context.Context) error {
	// 这里保持与 deploy/mysql/initdb 的表结构一致，服务本地启动可自动建表。
	statements := []string{
		`CREATE TABLE IF NOT EXISTS rooms (
			app_code VARCHAR(32) NOT NULL,
			room_id VARCHAR(64) NOT NULL,
			room_short_id VARCHAR(32) NOT NULL DEFAULT '',
			owner_user_id BIGINT NOT NULL,
			seat_count INT NOT NULL,
			mode VARCHAR(64) NOT NULL,
			status VARCHAR(32) NOT NULL,
			room_password_hash VARCHAR(128) NOT NULL DEFAULT '',
			close_reason VARCHAR(512) NOT NULL DEFAULT '',
			closed_by_admin_id BIGINT UNSIGNED NOT NULL DEFAULT 0,
			closed_by_admin_name VARCHAR(128) NOT NULL DEFAULT '',
			closed_at_ms BIGINT NOT NULL DEFAULT 0,
			visible_region_id BIGINT NOT NULL DEFAULT 0,
			created_at_ms BIGINT NOT NULL,
			updated_at_ms BIGINT NOT NULL,
			PRIMARY KEY (app_code, room_id),
			UNIQUE KEY uk_rooms_short_id (app_code, room_short_id),
			UNIQUE KEY uk_rooms_owner_user (app_code, owner_user_id),
			KEY idx_rooms_region_status (app_code, visible_region_id, status)
		) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`,
		`CREATE TABLE IF NOT EXISTS room_list_entries (
			app_code VARCHAR(32) NOT NULL,
			room_id VARCHAR(64) NOT NULL,
			room_short_id VARCHAR(32) NOT NULL DEFAULT '',
			visible_region_id BIGINT NOT NULL DEFAULT 0,
			owner_country_code VARCHAR(3) NOT NULL DEFAULT '',
			owner_user_id BIGINT NOT NULL,
			owner_vip_level INT NOT NULL DEFAULT 0,
			owner_vip_expires_at_ms BIGINT NOT NULL DEFAULT 0,
			owner_vip_fallback_level INT NOT NULL DEFAULT 0,
			owner_vip_fallback_expires_at_ms BIGINT NOT NULL DEFAULT 0,
			title VARCHAR(128) NOT NULL DEFAULT '',
			cover_url VARCHAR(512) NOT NULL DEFAULT '',
			mode VARCHAR(64) NOT NULL,
			status VARCHAR(32) NOT NULL,
			locked BOOLEAN NOT NULL DEFAULT FALSE,
			close_reason VARCHAR(512) NOT NULL DEFAULT '',
			closed_by_admin_id BIGINT UNSIGNED NOT NULL DEFAULT 0,
			closed_by_admin_name VARCHAR(128) NOT NULL DEFAULT '',
			closed_at_ms BIGINT NOT NULL DEFAULT 0,
			heat BIGINT NOT NULL DEFAULT 0,
			weekly_contribution BIGINT NOT NULL DEFAULT 0,
			contribution_week_start_ms BIGINT NOT NULL DEFAULT 0,
			online_count INT NOT NULL DEFAULT 0,
			seat_count INT NOT NULL DEFAULT 0,
			occupied_seat_count INT NOT NULL DEFAULT 0,
			sort_score BIGINT NOT NULL DEFAULT 0,
			rocket_near_completion BOOLEAN NOT NULL DEFAULT FALSE,
			has_active_red_packet BOOLEAN NOT NULL DEFAULT FALSE,
			active_red_packet_expires_at_ms BIGINT NOT NULL DEFAULT 0,
			room_border_json JSON NULL,
			room_name_style_json JSON NULL,
			created_at_ms BIGINT NOT NULL,
			updated_at_ms BIGINT NOT NULL,
			PRIMARY KEY (app_code, room_id),
			KEY idx_room_list_short_id (app_code, room_short_id),
			KEY idx_room_list_region_hot (app_code, visible_region_id, status, sort_score DESC, room_id),
			KEY idx_room_list_region_new (app_code, visible_region_id, status, created_at_ms DESC, room_id),
			KEY idx_room_list_region_country_hot (app_code, visible_region_id, owner_country_code, status, sort_score DESC, room_id),
			KEY idx_room_list_region_country_new (app_code, visible_region_id, owner_country_code, status, created_at_ms DESC, room_id),
			KEY idx_room_list_weekly_contribution (app_code, status, visible_region_id, weekly_contribution DESC, created_at_ms DESC, room_id),
			KEY idx_room_list_owner (app_code, owner_user_id, updated_at_ms)
		) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`,
		`CREATE TABLE IF NOT EXISTS room_user_presence (
			app_code VARCHAR(32) NOT NULL,
			user_id BIGINT NOT NULL,
			room_id VARCHAR(64) NOT NULL,
			role VARCHAR(32) NOT NULL,
			publish_state VARCHAR(32) NOT NULL DEFAULT '',
			mic_session_id VARCHAR(128) NOT NULL DEFAULT '',
			room_version BIGINT NOT NULL,
			status VARCHAR(32) NOT NULL,
			joined_at_ms BIGINT NOT NULL DEFAULT 0,
			last_seen_at_ms BIGINT NOT NULL DEFAULT 0,
			updated_at_ms BIGINT NOT NULL,
			PRIMARY KEY (app_code, user_id),
			KEY idx_room_user_presence_room (app_code, room_id, status),
			KEY idx_room_user_presence_updated (app_code, status, updated_at_ms)
		) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`,
		`CREATE TABLE IF NOT EXISTS room_user_gift_stats (
			app_code VARCHAR(32) NOT NULL,
			room_id VARCHAR(64) NOT NULL,
			user_id BIGINT NOT NULL,
			gift_value BIGINT NOT NULL DEFAULT 0,
			last_gift_at_ms BIGINT NOT NULL DEFAULT 0,
			created_at_ms BIGINT NOT NULL,
			updated_at_ms BIGINT NOT NULL,
			PRIMARY KEY (app_code, room_id, user_id),
			KEY idx_room_user_gift_stats_room_value (app_code, room_id, gift_value DESC, last_gift_at_ms DESC, user_id)
		) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`,
		`CREATE TABLE IF NOT EXISTS room_user_contribution_period_stats (
			app_code VARCHAR(32) NOT NULL,
			room_id VARCHAR(64) NOT NULL,
			period VARCHAR(16) NOT NULL,
			period_start_ms BIGINT NOT NULL,
			period_end_ms BIGINT NOT NULL,
			user_id BIGINT NOT NULL,
			gift_value BIGINT NOT NULL DEFAULT 0,
			last_gift_at_ms BIGINT NOT NULL DEFAULT 0,
			created_at_ms BIGINT NOT NULL,
			updated_at_ms BIGINT NOT NULL,
			PRIMARY KEY (app_code, room_id, period, period_start_ms, user_id),
			KEY idx_room_contribution_period_rank (app_code, room_id, period, period_start_ms, gift_value DESC, last_gift_at_ms DESC, user_id)
		) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`,
		`CREATE TABLE IF NOT EXISTS room_background_images (
			app_code VARCHAR(32) NOT NULL,
			background_id BIGINT NOT NULL AUTO_INCREMENT,
			room_id VARCHAR(64) NOT NULL,
			image_url VARCHAR(1024) NOT NULL,
			command_id VARCHAR(128) NULL,
			object_key VARCHAR(512) NOT NULL DEFAULT '',
			content_type VARCHAR(64) NOT NULL DEFAULT '',
			media_format VARCHAR(16) NOT NULL DEFAULT '',
			size_bytes BIGINT NOT NULL DEFAULT 0,
			width INT NOT NULL DEFAULT 0,
			height INT NOT NULL DEFAULT 0,
			animated BOOLEAN NOT NULL DEFAULT FALSE,
			frame_count INT NOT NULL DEFAULT 0,
			duration_ms BIGINT NOT NULL DEFAULT 0,
			poster_url VARCHAR(1024) NOT NULL DEFAULT '',
			thumbnail_url VARCHAR(1024) NOT NULL DEFAULT '',
			sha256 CHAR(64) NOT NULL DEFAULT '',
			media_status VARCHAR(32) NOT NULL DEFAULT '',
			created_by_user_id BIGINT NOT NULL,
			created_at_ms BIGINT NOT NULL,
			updated_at_ms BIGINT NOT NULL,
			PRIMARY KEY (background_id),
			UNIQUE KEY uk_room_background_url (app_code, room_id, image_url),
			UNIQUE KEY uk_room_background_command (app_code, room_id, command_id),
			KEY idx_room_background_list (app_code, room_id, created_at_ms DESC, background_id DESC)
		) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`,
		`CREATE TABLE IF NOT EXISTS room_media_upload_registrations (
			app_code VARCHAR(32) NOT NULL,
			room_id VARCHAR(64) NOT NULL,
			command_id VARCHAR(128) NOT NULL,
			actor_user_id BIGINT NOT NULL,
			purpose VARCHAR(32) NOT NULL,
			media_payload_sha256 CHAR(64) NOT NULL,
			expected_object_key VARCHAR(512) NOT NULL,
			object_url VARCHAR(1024) NOT NULL DEFAULT '',
			status VARCHAR(32) NOT NULL DEFAULT 'authorized',
			content_type VARCHAR(64) NOT NULL,
			media_format VARCHAR(16) NOT NULL,
			size_bytes BIGINT NOT NULL,
			width INT NOT NULL,
			height INT NOT NULL,
			animated BOOLEAN NOT NULL DEFAULT FALSE,
			frame_count INT NOT NULL,
			duration_ms BIGINT NOT NULL DEFAULT 0,
			sha256 CHAR(64) NOT NULL,
			authorized_room_version BIGINT NOT NULL,
			created_at_ms BIGINT NOT NULL,
			updated_at_ms BIGINT NOT NULL,
			PRIMARY KEY (app_code, room_id, command_id),
			UNIQUE KEY uk_room_media_upload_object (app_code, expected_object_key),
			KEY idx_room_media_upload_status_created (app_code, status, created_at_ms)
		) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`,
		`CREATE TABLE IF NOT EXISTS room_decoration_assignments (
			app_code VARCHAR(32) NOT NULL,
			room_id VARCHAR(64) NOT NULL,
			decoration_type VARCHAR(32) NOT NULL,
			owner_user_id BIGINT NOT NULL,
			benefit_code VARCHAR(64) NOT NULL,
			resource_id BIGINT NOT NULL,
			entitlement_id VARCHAR(128) NOT NULL DEFAULT '',
			expires_at_ms BIGINT NOT NULL DEFAULT 0,
			updated_at_ms BIGINT NOT NULL,
			PRIMARY KEY (app_code, room_id, decoration_type),
			KEY idx_room_decoration_resource (app_code, resource_id, room_id, decoration_type),
			KEY idx_room_decoration_owner (app_code, owner_user_id, room_id, decoration_type),
			KEY idx_room_decoration_expiry (app_code, expires_at_ms, room_id, decoration_type)
		) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`,
		`CREATE TABLE IF NOT EXISTS room_wallet_event_consumption (
			app_code VARCHAR(32) NOT NULL,
			event_id VARCHAR(128) NOT NULL,
			event_type VARCHAR(64) NOT NULL,
			consumed_at_ms BIGINT NOT NULL,
			PRIMARY KEY (app_code, event_id),
			KEY idx_room_wallet_event_consumed (app_code, consumed_at_ms)
		) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`,
		`CREATE TABLE IF NOT EXISTS room_red_packet_projections (
			app_code VARCHAR(32) NOT NULL,
			packet_id VARCHAR(128) NOT NULL,
			room_id VARCHAR(64) NOT NULL,
			status VARCHAR(32) NOT NULL,
			remaining_amount BIGINT NOT NULL DEFAULT 0,
			remaining_count INT NOT NULL DEFAULT 0,
			expires_at_ms BIGINT NOT NULL DEFAULT 0,
			event_rank INT NOT NULL DEFAULT 0,
			event_occurred_at_ms BIGINT NOT NULL,
			updated_at_ms BIGINT NOT NULL,
			PRIMARY KEY (app_code, packet_id),
			KEY idx_room_red_packet_active (app_code, room_id, status, expires_at_ms)
		) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`,
		`CREATE TABLE IF NOT EXISTS room_snapshots (
			app_code VARCHAR(32) NOT NULL,
			room_id VARCHAR(64) NOT NULL,
			room_version BIGINT NOT NULL,
			payload LONGBLOB NOT NULL,
			created_at_ms BIGINT NOT NULL,
			updated_at_ms BIGINT NOT NULL,
			PRIMARY KEY (app_code, room_id),
			INDEX idx_room_snapshots_version (app_code, room_id, room_version)
		) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`,
		`CREATE TABLE IF NOT EXISTS room_command_log (
			app_code VARCHAR(32) NOT NULL,
			id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,
			room_id VARCHAR(64) NOT NULL,
			room_version BIGINT NOT NULL,
			command_id VARCHAR(128) NOT NULL,
			command_type VARCHAR(64) NOT NULL,
			owner_node_id VARCHAR(128) NOT NULL DEFAULT '',
			lease_token VARCHAR(128) NOT NULL DEFAULT '',
			payload LONGBLOB NOT NULL,
			result_payload LONGBLOB NULL,
			replayable BOOLEAN NOT NULL,
			created_at_ms BIGINT NOT NULL,
			UNIQUE KEY uk_room_command (app_code, room_id, command_id),
			KEY idx_room_command_replay (app_code, room_id, room_version)
		) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`,
		`CREATE TABLE IF NOT EXISTS room_gift_operations (
			app_code VARCHAR(32) NOT NULL,
			room_id VARCHAR(64) NOT NULL,
			command_id VARCHAR(128) NOT NULL,
			actor_user_id BIGINT NOT NULL,
			command_payload LONGBLOB NOT NULL,
			request_payload LONGBLOB NOT NULL,
			status VARCHAR(32) NOT NULL,
			billing_payload LONGBLOB NULL,
			lucky_payload LONGBLOB NULL,
			result_payload LONGBLOB NULL,
			last_error TEXT NULL,
			retry_count INT NOT NULL DEFAULT 0,
			next_retry_at_ms BIGINT NOT NULL DEFAULT 0,
			worker_id VARCHAR(128) NOT NULL DEFAULT '',
			lock_until_ms BIGINT NULL,
			created_at_ms BIGINT NOT NULL,
			updated_at_ms BIGINT NOT NULL,
			PRIMARY KEY (app_code, room_id, command_id),
			KEY idx_room_gift_operation_recovery (status, next_retry_at_ms, lock_until_ms, updated_at_ms)
		) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`,
		`CREATE TABLE IF NOT EXISTS room_outbox (
			app_code VARCHAR(32) NOT NULL,
			event_id VARCHAR(128) NOT NULL,
			event_type VARCHAR(64) NOT NULL,
			room_id VARCHAR(64) NOT NULL,
			status VARCHAR(32) NOT NULL,
			worker_id VARCHAR(128) NOT NULL DEFAULT '',
			lock_until_ms BIGINT NULL,
			envelope LONGBLOB NOT NULL,
			created_at_ms BIGINT NOT NULL,
			retry_count INT NOT NULL DEFAULT 0,
			next_retry_at_ms BIGINT NOT NULL DEFAULT 0,
			last_error TEXT NULL,
			updated_at_ms BIGINT NOT NULL,
			PRIMARY KEY (app_code, event_id),
			KEY idx_room_outbox_pending (app_code, status, next_retry_at_ms, created_at_ms, event_id),
			KEY idx_room_outbox_claim (app_code, status, lock_until_ms, created_at_ms, event_id),
			KEY idx_room_outbox_event_created (app_code, event_type, created_at_ms, event_id),
			KEY idx_room_outbox_retention (app_code, status, updated_at_ms, event_id),
			KEY idx_room_outbox_room_event (app_code, room_id, event_type, created_at_ms, event_id)
		) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`,
		`CREATE TABLE IF NOT EXISTS room_user_feed_entries (
			app_code VARCHAR(32) NOT NULL,
			user_id BIGINT NOT NULL,
			feed_type VARCHAR(32) NOT NULL,
			room_id VARCHAR(64) NOT NULL,
			created_at_ms BIGINT NOT NULL,
			updated_at_ms BIGINT NOT NULL,
			PRIMARY KEY (app_code, user_id, feed_type, room_id),
			KEY idx_room_user_feed_list (app_code, user_id, feed_type, updated_at_ms DESC, room_id)
		) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`,
		`CREATE TABLE IF NOT EXISTS room_follows (
			app_code VARCHAR(32) NOT NULL,
			user_id BIGINT NOT NULL,
			room_id VARCHAR(64) NOT NULL,
			status VARCHAR(32) NOT NULL,
			followed_at_ms BIGINT NOT NULL,
			updated_at_ms BIGINT NOT NULL,
			PRIMARY KEY (app_code, user_id, room_id),
			KEY idx_room_follows_user_list (app_code, user_id, status, followed_at_ms DESC, room_id),
			KEY idx_room_follows_room (app_code, room_id, status)
		) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`,
		`CREATE TABLE IF NOT EXISTS room_region_pins (
			id BIGINT NOT NULL AUTO_INCREMENT,
			app_code VARCHAR(32) NOT NULL,
			visible_region_id BIGINT NOT NULL,
			pin_type VARCHAR(32) NOT NULL DEFAULT 'region',
			room_id VARCHAR(64) NOT NULL,
			weight BIGINT NOT NULL DEFAULT 0,
			status VARCHAR(32) NOT NULL,
			pinned_at_ms BIGINT NOT NULL,
			expires_at_ms BIGINT NOT NULL,
			cancelled_at_ms BIGINT NOT NULL DEFAULT 0,
			created_by_admin_id BIGINT UNSIGNED NOT NULL DEFAULT 0,
			cancelled_by_admin_id BIGINT UNSIGNED NOT NULL DEFAULT 0,
			created_at_ms BIGINT NOT NULL,
			updated_at_ms BIGINT NOT NULL,
			PRIMARY KEY (id),
			UNIQUE KEY uk_room_region_pins_scope (app_code, pin_type, visible_region_id, room_id),
			KEY idx_room_region_pins_active_scope (app_code, pin_type, visible_region_id, status, pinned_at_ms, expires_at_ms, weight DESC, room_id),
			KEY idx_room_region_pins_room (app_code, room_id, status)
		) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='房间置顶表'`,
		`CREATE TABLE IF NOT EXISTS room_seat_configs (
			app_code VARCHAR(32) NOT NULL,
			allowed_seat_counts VARCHAR(128) NOT NULL,
			default_seat_count INT NOT NULL,
			created_at_ms BIGINT NOT NULL,
			updated_at_ms BIGINT NOT NULL,
			PRIMARY KEY (app_code)
		) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`,
		`CREATE TABLE IF NOT EXISTS room_rocket_configs (
			app_code VARCHAR(32) NOT NULL,
			enabled TINYINT(1) NOT NULL DEFAULT 0,
			config_version BIGINT NOT NULL DEFAULT 1,
			fuel_source VARCHAR(32) NOT NULL,
			launch_delay_ms BIGINT NOT NULL,
			broadcast_enabled TINYINT(1) NOT NULL DEFAULT 1,
			broadcast_scope VARCHAR(32) NOT NULL,
			broadcast_delay_ms BIGINT NOT NULL DEFAULT 0,
			reward_stack_policy VARCHAR(32) NOT NULL,
			levels_json JSON NOT NULL,
			gift_fuel_rules_json JSON NOT NULL,
			updated_by_admin_id BIGINT UNSIGNED NOT NULL DEFAULT 0,
			created_at_ms BIGINT NOT NULL,
			updated_at_ms BIGINT NOT NULL,
			PRIMARY KEY (app_code),
			KEY idx_room_rocket_enabled (app_code, enabled)
		) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`,
		`CREATE TABLE IF NOT EXISTS room_robot_rooms (
			app_code VARCHAR(32) NOT NULL,
			room_id VARCHAR(64) NOT NULL,
			room_short_id VARCHAR(32) NOT NULL DEFAULT '',
			title VARCHAR(128) NOT NULL DEFAULT '',
			cover_url VARCHAR(512) NOT NULL DEFAULT '',
			visible_region_id BIGINT NOT NULL DEFAULT 0,
			status VARCHAR(32) NOT NULL,
			owner_robot_user_id BIGINT NOT NULL,
			robot_user_ids_json JSON NOT NULL,
			active_robot_count INT NOT NULL DEFAULT 0,
			seat_count INT NOT NULL DEFAULT 10,
			gift_ids_json JSON NOT NULL,
			lucky_gift_ids_json JSON NOT NULL,
			normal_gift_interval_ms BIGINT NOT NULL,
			lucky_combo_min BIGINT NOT NULL,
			lucky_combo_max BIGINT NOT NULL,
			lucky_pause_min_ms BIGINT NOT NULL,
			lucky_pause_max_ms BIGINT NOT NULL,
			robot_stay_min_ms BIGINT NOT NULL DEFAULT 180000,
			robot_stay_max_ms BIGINT NOT NULL DEFAULT 600000,
			robot_replace_min_ms BIGINT NOT NULL DEFAULT 0,
			robot_replace_max_ms BIGINT NOT NULL DEFAULT 60000,
			max_gift_senders BIGINT NOT NULL DEFAULT 1,
			created_by_admin_id BIGINT UNSIGNED NOT NULL DEFAULT 0,
			created_at_ms BIGINT NOT NULL,
			updated_at_ms BIGINT NOT NULL,
			PRIMARY KEY (app_code, room_id),
			KEY idx_room_robot_rooms_status (app_code, status, updated_at_ms, room_id),
			KEY idx_room_robot_rooms_owner (app_code, owner_robot_user_id)
		) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`,
		`CREATE TABLE IF NOT EXISTS room_human_robot_configs (
			app_code VARCHAR(32) NOT NULL,
			enabled BOOLEAN NOT NULL DEFAULT FALSE,
			candidate_room_max_online INT NOT NULL DEFAULT 5,
			room_full_stop_online INT NOT NULL DEFAULT 10,
			room_target_min_online INT NOT NULL DEFAULT 8,
			room_target_max_online INT NOT NULL DEFAULT 10,
			robot_stay_min_ms BIGINT NOT NULL DEFAULT 60000,
			robot_stay_max_ms BIGINT NOT NULL DEFAULT 600000,
			robot_replace_min_ms BIGINT NOT NULL DEFAULT 60000,
			robot_replace_max_ms BIGINT NOT NULL DEFAULT 180000,
			normal_gift_interval_ms BIGINT NOT NULL DEFAULT 10000,
			normal_gift_interval_min_ms BIGINT NOT NULL DEFAULT 1000,
			normal_gift_interval_max_ms BIGINT NOT NULL DEFAULT 20000,
			gift_ids_json JSON NOT NULL,
			lucky_gift_ids_json JSON NOT NULL,
			super_lucky_gift_ids_json JSON NOT NULL,
			lucky_combo_min BIGINT NOT NULL DEFAULT 1,
			lucky_combo_max BIGINT NOT NULL DEFAULT 3,
			lucky_pause_min_ms BIGINT NOT NULL DEFAULT 5000,
			lucky_pause_max_ms BIGINT NOT NULL DEFAULT 20000,
			max_gift_senders BIGINT NOT NULL DEFAULT 1,
			country_pools_json JSON NOT NULL,
			allowed_owner_ids_json JSON NOT NULL,
			country_limit_enabled BOOLEAN NOT NULL DEFAULT FALSE,
			country_rules_json JSON NOT NULL,
			updated_by_admin_id BIGINT UNSIGNED NOT NULL DEFAULT 0,
			created_at_ms BIGINT NOT NULL,
			updated_at_ms BIGINT NOT NULL,
			PRIMARY KEY (app_code),
			KEY idx_room_human_robot_enabled (app_code, enabled)
		) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`,
		`INSERT IGNORE INTO room_seat_configs (app_code, allowed_seat_counts, default_seat_count, created_at_ms, updated_at_ms)
		 VALUES ('lalu', '[10,15,20,25,30]', 15, 0, 0)`,
		`INSERT IGNORE INTO room_rocket_configs (
			app_code,
			enabled,
			config_version,
			fuel_source,
			launch_delay_ms,
			broadcast_enabled,
			broadcast_scope,
			broadcast_delay_ms,
			reward_stack_policy,
			levels_json,
			gift_fuel_rules_json,
			updated_by_admin_id,
			created_at_ms,
			updated_at_ms
		) VALUES (
			'lalu',
			0,
			1,
			'heat_value',
			30000,
			1,
			'region',
			0,
			'allow_stack',
			JSON_ARRAY(
				JSON_OBJECT('level', 1, 'fuelThreshold', 1000, 'coverUrl', '', 'animationUrl', '', 'launchAnimationUrl', '', 'launchedImageUrl', '', 'inRoomRewards', JSON_ARRAY(), 'top1Rewards', JSON_ARRAY(), 'igniterRewards', JSON_ARRAY()),
				JSON_OBJECT('level', 2, 'fuelThreshold', 3000, 'coverUrl', '', 'animationUrl', '', 'launchAnimationUrl', '', 'launchedImageUrl', '', 'inRoomRewards', JSON_ARRAY(), 'top1Rewards', JSON_ARRAY(), 'igniterRewards', JSON_ARRAY()),
				JSON_OBJECT('level', 3, 'fuelThreshold', 6000, 'coverUrl', '', 'animationUrl', '', 'launchAnimationUrl', '', 'launchedImageUrl', '', 'inRoomRewards', JSON_ARRAY(), 'top1Rewards', JSON_ARRAY(), 'igniterRewards', JSON_ARRAY()),
				JSON_OBJECT('level', 4, 'fuelThreshold', 10000, 'coverUrl', '', 'animationUrl', '', 'launchAnimationUrl', '', 'launchedImageUrl', '', 'inRoomRewards', JSON_ARRAY(), 'top1Rewards', JSON_ARRAY(), 'igniterRewards', JSON_ARRAY()),
				JSON_OBJECT('level', 5, 'fuelThreshold', 15000, 'coverUrl', '', 'animationUrl', '', 'launchAnimationUrl', '', 'launchedImageUrl', '', 'inRoomRewards', JSON_ARRAY(), 'top1Rewards', JSON_ARRAY(), 'igniterRewards', JSON_ARRAY())
			),
			JSON_ARRAY(),
			0,
			0,
			0
		)`,
		`ALTER TABLE rooms MODIFY COLUMN app_code VARCHAR(32) NOT NULL`,
		`ALTER TABLE room_list_entries MODIFY COLUMN app_code VARCHAR(32) NOT NULL`,
		`ALTER TABLE room_user_presence MODIFY COLUMN app_code VARCHAR(32) NOT NULL`,
		`ALTER TABLE room_snapshots MODIFY COLUMN app_code VARCHAR(32) NOT NULL`,
		`ALTER TABLE room_command_log MODIFY COLUMN app_code VARCHAR(32) NOT NULL`,
		`ALTER TABLE room_gift_operations MODIFY COLUMN app_code VARCHAR(32) NOT NULL`,
		`ALTER TABLE room_outbox MODIFY COLUMN app_code VARCHAR(32) NOT NULL`,
		`ALTER TABLE room_user_feed_entries MODIFY COLUMN app_code VARCHAR(32) NOT NULL`,
		`ALTER TABLE room_follows MODIFY COLUMN app_code VARCHAR(32) NOT NULL`,
		`ALTER TABLE room_region_pins MODIFY COLUMN app_code VARCHAR(32) NOT NULL`,
		`ALTER TABLE room_seat_configs MODIFY COLUMN app_code VARCHAR(32) NOT NULL`,
		`ALTER TABLE room_rocket_configs MODIFY COLUMN app_code VARCHAR(32) NOT NULL`,
		`ALTER TABLE room_robot_rooms MODIFY COLUMN app_code VARCHAR(32) NOT NULL`,
	}

	for _, statement := range statements {
		// 每条 DDL 独立执行，失败时返回具体 SQL 错误给启动流程。
		if _, err := r.db.ExecContext(ctx, statement); err != nil {
			return err
		}
	}

	if err := r.ensureRoomListLockSchema(ctx); err != nil {
		return err
	}
	if err := r.ensureRoomListCountrySchema(ctx); err != nil {
		return err
	}
	if err := r.ensureRoomPasswordSchema(ctx); err != nil {
		return err
	}
	if err := r.ensureRoomAdminCloseSchema(ctx); err != nil {
		return err
	}
	if err := r.ensureRoomWeeklyContributionSchema(ctx); err != nil {
		return err
	}
	if err := r.ensureRoomUserPresenceSchema(ctx); err != nil {
		return err
	}
	if err := r.ensureRoomPinSchema(ctx); err != nil {
		return err
	}
	if err := r.ensureRobotRoomBehaviorSchema(ctx); err != nil {
		return err
	}
	if err := r.ensureHumanRobotConfigBehaviorSchema(ctx); err != nil {
		return err
	}
	if err := r.ensureGiftOperationSchema(ctx); err != nil {
		return err
	}
	if err := r.ensureRoomVIPMediaSchema(ctx); err != nil {
		return err
	}

	return r.ensureOutboxRetrySchema(ctx)
}

func (r *Repository) ensureRoomVIPMediaSchema(ctx context.Context) error {
	backgroundColumns := []struct{ name, ddl string }{
		{"command_id", "ALTER TABLE room_background_images ADD COLUMN command_id VARCHAR(128) NULL"},
		{"object_key", "ALTER TABLE room_background_images ADD COLUMN object_key VARCHAR(512) NOT NULL DEFAULT ''"},
		{"content_type", "ALTER TABLE room_background_images ADD COLUMN content_type VARCHAR(64) NOT NULL DEFAULT ''"},
		{"media_format", "ALTER TABLE room_background_images ADD COLUMN media_format VARCHAR(16) NOT NULL DEFAULT ''"},
		{"size_bytes", "ALTER TABLE room_background_images ADD COLUMN size_bytes BIGINT NOT NULL DEFAULT 0"},
		{"width", "ALTER TABLE room_background_images ADD COLUMN width INT NOT NULL DEFAULT 0"},
		{"height", "ALTER TABLE room_background_images ADD COLUMN height INT NOT NULL DEFAULT 0"},
		{"animated", "ALTER TABLE room_background_images ADD COLUMN animated BOOLEAN NOT NULL DEFAULT FALSE"},
		{"frame_count", "ALTER TABLE room_background_images ADD COLUMN frame_count INT NOT NULL DEFAULT 0"},
		{"duration_ms", "ALTER TABLE room_background_images ADD COLUMN duration_ms BIGINT NOT NULL DEFAULT 0"},
		{"poster_url", "ALTER TABLE room_background_images ADD COLUMN poster_url VARCHAR(1024) NOT NULL DEFAULT ''"},
		{"thumbnail_url", "ALTER TABLE room_background_images ADD COLUMN thumbnail_url VARCHAR(1024) NOT NULL DEFAULT ''"},
		{"sha256", "ALTER TABLE room_background_images ADD COLUMN sha256 CHAR(64) NOT NULL DEFAULT ''"},
		{"media_status", "ALTER TABLE room_background_images ADD COLUMN media_status VARCHAR(32) NOT NULL DEFAULT ''"},
	}
	for _, column := range backgroundColumns {
		exists, err := r.columnExists(ctx, "room_background_images", column.name)
		if err != nil {
			return err
		}
		if !exists {
			if _, err := r.db.ExecContext(ctx, column.ddl); err != nil {
				return err
			}
		}
	}
	for _, column := range []struct{ name, ddl string }{
		{"room_border_json", "ALTER TABLE room_list_entries ADD COLUMN room_border_json JSON NULL"},
		{"room_name_style_json", "ALTER TABLE room_list_entries ADD COLUMN room_name_style_json JSON NULL"},
		// 两列不参与索引和排序，MySQL 8 使用 INSTANT 只改数据字典，不重写高频房间列表表。
		{"owner_vip_level", "ALTER TABLE room_list_entries ADD COLUMN owner_vip_level INT NOT NULL DEFAULT 0, ALGORITHM=INSTANT"},
		{"owner_vip_expires_at_ms", "ALTER TABLE room_list_entries ADD COLUMN owner_vip_expires_at_ms BIGINT NOT NULL DEFAULT 0, ALGORITHM=INSTANT"},
		{"owner_vip_fallback_level", "ALTER TABLE room_list_entries ADD COLUMN owner_vip_fallback_level INT NOT NULL DEFAULT 0, ALGORITHM=INSTANT"},
		{"owner_vip_fallback_expires_at_ms", "ALTER TABLE room_list_entries ADD COLUMN owner_vip_fallback_expires_at_ms BIGINT NOT NULL DEFAULT 0, ALGORITHM=INSTANT"},
	} {
		exists, err := r.columnExists(ctx, "room_list_entries", column.name)
		if err != nil {
			return err
		}
		if !exists {
			if _, err := r.db.ExecContext(ctx, column.ddl); err != nil {
				return err
			}
		}
	}
	hasCommandIndex, err := r.indexExists(ctx, "room_background_images", "uk_room_background_command")
	if err != nil {
		return err
	}
	if !hasCommandIndex {
		_, err = r.db.ExecContext(ctx, "ALTER TABLE room_background_images ADD UNIQUE KEY uk_room_background_command (app_code, room_id, command_id)")
	}
	return err
}

func (r *Repository) ensureGiftOperationSchema(ctx context.Context) error {
	// CREATE TABLE IF NOT EXISTS 不会给历史 room_command_log 补列；auto_migrate 必须显式探测，
	// 否则新二进制启动后第一笔 GetCommand 会因 result_payload 不存在而直接 SQL 失败。
	hasResultPayload, err := r.columnExists(ctx, "room_command_log", "result_payload")
	if err != nil {
		return err
	}
	if !hasResultPayload {
		if _, err := r.db.ExecContext(ctx, `ALTER TABLE room_command_log ADD COLUMN result_payload LONGBLOB NULL AFTER payload`); err != nil {
			return err
		}
	}
	return nil
}

func (r *Repository) ensureHumanRobotConfigBehaviorSchema(ctx context.Context) error {
	columns := []struct {
		name      string
		statement string
	}{
		{name: "normal_gift_interval_min_ms", statement: `ALTER TABLE room_human_robot_configs ADD COLUMN normal_gift_interval_min_ms BIGINT NOT NULL DEFAULT 1000 AFTER normal_gift_interval_ms`},
		{name: "normal_gift_interval_max_ms", statement: `ALTER TABLE room_human_robot_configs ADD COLUMN normal_gift_interval_max_ms BIGINT NOT NULL DEFAULT 20000 AFTER normal_gift_interval_min_ms`},
		{name: "room_target_min_online", statement: `ALTER TABLE room_human_robot_configs ADD COLUMN room_target_min_online INT NOT NULL DEFAULT 8 AFTER room_full_stop_online`},
		{name: "room_target_max_online", statement: `ALTER TABLE room_human_robot_configs ADD COLUMN room_target_max_online INT NOT NULL DEFAULT 10 AFTER room_target_min_online`},
		{name: "allowed_owner_ids_json", statement: `ALTER TABLE room_human_robot_configs ADD COLUMN allowed_owner_ids_json JSON NULL AFTER country_pools_json`},
		{name: "country_limit_enabled", statement: `ALTER TABLE room_human_robot_configs ADD COLUMN country_limit_enabled BOOLEAN NOT NULL DEFAULT FALSE AFTER allowed_owner_ids_json`},
		{name: "country_rules_json", statement: `ALTER TABLE room_human_robot_configs ADD COLUMN country_rules_json JSON NULL AFTER country_limit_enabled`},
	}
	for _, column := range columns {
		exists, err := r.columnExists(ctx, "room_human_robot_configs", column.name)
		if err != nil {
			return err
		}
		if exists {
			continue
		}
		if _, err := r.db.ExecContext(ctx, column.statement); err != nil {
			return err
		}
	}
	return nil
}

func (r *Repository) ensureRobotRoomBehaviorSchema(ctx context.Context) error {
	columns := []struct {
		name      string
		statement string
	}{
		{name: "active_robot_count", statement: `ALTER TABLE room_robot_rooms ADD COLUMN active_robot_count INT NOT NULL DEFAULT 0 AFTER robot_user_ids_json`},
		{name: "seat_count", statement: `ALTER TABLE room_robot_rooms ADD COLUMN seat_count INT NOT NULL DEFAULT 10 AFTER active_robot_count`},
		{name: "robot_stay_min_ms", statement: `ALTER TABLE room_robot_rooms ADD COLUMN robot_stay_min_ms BIGINT NOT NULL DEFAULT 180000 AFTER lucky_pause_max_ms`},
		{name: "robot_stay_max_ms", statement: `ALTER TABLE room_robot_rooms ADD COLUMN robot_stay_max_ms BIGINT NOT NULL DEFAULT 600000 AFTER robot_stay_min_ms`},
		{name: "robot_replace_min_ms", statement: `ALTER TABLE room_robot_rooms ADD COLUMN robot_replace_min_ms BIGINT NOT NULL DEFAULT 0 AFTER robot_stay_max_ms`},
		{name: "robot_replace_max_ms", statement: `ALTER TABLE room_robot_rooms ADD COLUMN robot_replace_max_ms BIGINT NOT NULL DEFAULT 60000 AFTER robot_replace_min_ms`},
		{name: "max_gift_senders", statement: `ALTER TABLE room_robot_rooms ADD COLUMN max_gift_senders BIGINT NOT NULL DEFAULT 1 AFTER robot_replace_max_ms`},
	}
	for _, column := range columns {
		exists, err := r.columnExists(ctx, "room_robot_rooms", column.name)
		if err != nil {
			return err
		}
		if exists {
			continue
		}
		if _, err := r.db.ExecContext(ctx, column.statement); err != nil {
			return err
		}
	}
	return nil
}

func (r *Repository) ensureRoomWeeklyContributionSchema(ctx context.Context) error {
	columns := []struct {
		column    string
		statement string
	}{
		{
			column:    "weekly_contribution",
			statement: `ALTER TABLE room_list_entries ADD COLUMN weekly_contribution BIGINT NOT NULL DEFAULT 0 AFTER heat`,
		},
		{
			column:    "contribution_week_start_ms",
			statement: `ALTER TABLE room_list_entries ADD COLUMN contribution_week_start_ms BIGINT NOT NULL DEFAULT 0 AFTER weekly_contribution`,
		},
	}
	for _, item := range columns {
		hasColumn, err := r.columnExists(ctx, "room_list_entries", item.column)
		if err != nil {
			return err
		}
		if !hasColumn {
			if _, err := r.db.ExecContext(ctx, item.statement); err != nil {
				return err
			}
		}
	}
	hasIndex, err := r.indexExists(ctx, "room_list_entries", "idx_room_list_weekly_contribution")
	if err != nil {
		return err
	}
	if !hasIndex {
		// 后台按房间贡献排序时只读当前 UTC 周贡献；索引用于收敛 active/region 过滤后的排序成本。
		if _, err := r.db.ExecContext(ctx, `ALTER TABLE room_list_entries ADD INDEX idx_room_list_weekly_contribution (app_code, status, visible_region_id, weekly_contribution DESC, created_at_ms DESC, room_id)`); err != nil {
			return err
		}
	}
	return nil
}

func (r *Repository) ensureRoomPinSchema(ctx context.Context) error {
	hasColumn, err := r.columnExists(ctx, "room_region_pins", "pin_type")
	if err != nil {
		return err
	}
	if !hasColumn {
		// pin_type 把“全区置顶”和“区域置顶”的作用域写进同一 owner 表；历史数据默认都是区域置顶。
		if _, err := r.db.ExecContext(ctx, `ALTER TABLE room_region_pins ADD COLUMN pin_type VARCHAR(32) NOT NULL DEFAULT 'region' AFTER visible_region_id`); err != nil {
			return err
		}
	}
	hasScopeUnique, err := r.indexExists(ctx, "room_region_pins", "uk_room_region_pins_scope")
	if err != nil {
		return err
	}
	if !hasScopeUnique {
		if _, err := r.db.ExecContext(ctx, `ALTER TABLE room_region_pins ADD UNIQUE KEY uk_room_region_pins_scope (app_code, pin_type, visible_region_id, room_id)`); err != nil {
			return err
		}
	}
	hasLegacyUnique, err := r.indexExists(ctx, "room_region_pins", "uk_room_region_pins_room")
	if err != nil {
		return err
	}
	if hasLegacyUnique {
		// 新唯一键允许 region_id=0 的区域置顶和全区置顶并存；旧唯一键会错误地把两者互斥。
		if _, err := r.db.ExecContext(ctx, `ALTER TABLE room_region_pins DROP INDEX uk_room_region_pins_room`); err != nil {
			return err
		}
	}
	hasActiveScope, err := r.indexExists(ctx, "room_region_pins", "idx_room_region_pins_active_scope")
	if err != nil {
		return err
	}
	if !hasActiveScope {
		if _, err := r.db.ExecContext(ctx, `ALTER TABLE room_region_pins ADD INDEX idx_room_region_pins_active_scope (app_code, pin_type, visible_region_id, status, pinned_at_ms, expires_at_ms, weight DESC, room_id)`); err != nil {
			return err
		}
	}
	return nil
}

func (r *Repository) ensureRoomUserPresenceSchema(ctx context.Context) error {
	hasColumn, err := r.columnExists(ctx, "room_user_presence", "joined_at_ms")
	if err != nil {
		return err
	}
	if !hasColumn {
		if _, err := r.db.ExecContext(ctx, `ALTER TABLE room_user_presence ADD COLUMN joined_at_ms BIGINT NOT NULL DEFAULT 0 AFTER status`); err != nil {
			return err
		}
	}
	return nil
}

func (r *Repository) ensureRoomPasswordSchema(ctx context.Context) error {
	hasColumn, err := r.columnExists(ctx, "rooms", "room_password_hash")
	if err != nil {
		return err
	}
	if !hasColumn {
		// rooms 是最新快照恢复密码哈希的持久来源；列表只使用 locked 布尔投影。
		if _, err := r.db.ExecContext(ctx, `ALTER TABLE rooms ADD COLUMN room_password_hash VARCHAR(128) NOT NULL DEFAULT '' AFTER status`); err != nil {
			return err
		}
	}
	return nil
}

func (r *Repository) ensureRoomListLockSchema(ctx context.Context) error {
	hasColumn, err := r.columnExists(ctx, "room_list_entries", "locked")
	if err != nil {
		return err
	}
	if !hasColumn {
		// locked 是列表/搜索需要的纯投影字段，历史行默认 false，后续房间状态变更会刷新为真实值。
		if _, err := r.db.ExecContext(ctx, `ALTER TABLE room_list_entries ADD COLUMN locked BOOLEAN NOT NULL DEFAULT FALSE AFTER status`); err != nil {
			return err
		}
	}
	return nil
}

func (r *Repository) ensureRoomListCountrySchema(ctx context.Context) error {
	hasColumn, err := r.columnExists(ctx, "room_list_entries", "owner_country_code")
	if err != nil {
		return err
	}
	if !hasColumn {
		// owner_country_code 是房主国家快照，只服务发现页国家筛选；历史行先为空，后续房间投影刷新会补真实值。
		if _, err := r.db.ExecContext(ctx, `ALTER TABLE room_list_entries ADD COLUMN owner_country_code VARCHAR(3) NOT NULL DEFAULT '' AFTER visible_region_id`); err != nil {
			return err
		}
	}
	indexes := []struct {
		name      string
		statement string
	}{
		{
			name:      "idx_room_list_region_country_hot",
			statement: `ALTER TABLE room_list_entries ADD INDEX idx_room_list_region_country_hot (app_code, visible_region_id, owner_country_code, status, sort_score DESC, room_id)`,
		},
		{
			name:      "idx_room_list_region_country_new",
			statement: `ALTER TABLE room_list_entries ADD INDEX idx_room_list_region_country_new (app_code, visible_region_id, owner_country_code, status, created_at_ms DESC, room_id)`,
		},
	}
	for _, item := range indexes {
		hasIndex, err := r.indexExists(ctx, "room_list_entries", item.name)
		if err != nil {
			return err
		}
		if !hasIndex {
			if _, err := r.db.ExecContext(ctx, item.statement); err != nil {
				return err
			}
		}
	}
	return nil
}

func (r *Repository) ensureRoomAdminCloseSchema(ctx context.Context) error {
	columns := []struct {
		table     string
		column    string
		statement string
	}{
		{"rooms", "close_reason", `ALTER TABLE rooms ADD COLUMN close_reason VARCHAR(512) NOT NULL DEFAULT '' AFTER room_password_hash`},
		{"rooms", "closed_by_admin_id", `ALTER TABLE rooms ADD COLUMN closed_by_admin_id BIGINT UNSIGNED NOT NULL DEFAULT 0 AFTER close_reason`},
		{"rooms", "closed_by_admin_name", `ALTER TABLE rooms ADD COLUMN closed_by_admin_name VARCHAR(128) NOT NULL DEFAULT '' AFTER closed_by_admin_id`},
		{"rooms", "closed_at_ms", `ALTER TABLE rooms ADD COLUMN closed_at_ms BIGINT NOT NULL DEFAULT 0 AFTER closed_by_admin_name`},
		{"room_list_entries", "close_reason", `ALTER TABLE room_list_entries ADD COLUMN close_reason VARCHAR(512) NOT NULL DEFAULT '' AFTER locked`},
		{"room_list_entries", "closed_by_admin_id", `ALTER TABLE room_list_entries ADD COLUMN closed_by_admin_id BIGINT UNSIGNED NOT NULL DEFAULT 0 AFTER close_reason`},
		{"room_list_entries", "closed_by_admin_name", `ALTER TABLE room_list_entries ADD COLUMN closed_by_admin_name VARCHAR(128) NOT NULL DEFAULT '' AFTER closed_by_admin_id`},
		{"room_list_entries", "closed_at_ms", `ALTER TABLE room_list_entries ADD COLUMN closed_at_ms BIGINT NOT NULL DEFAULT 0 AFTER closed_by_admin_name`},
	}
	for _, item := range columns {
		hasColumn, err := r.columnExists(ctx, item.table, item.column)
		if err != nil {
			return err
		}
		if !hasColumn {
			if _, err := r.db.ExecContext(ctx, item.statement); err != nil {
				return err
			}
		}
	}
	return nil
}

func (r *Repository) ensureOutboxRetrySchema(ctx context.Context) error {
	hasColumn, err := r.columnExists(ctx, "room_outbox", "next_retry_at_ms")
	if err != nil {
		return err
	}
	if !hasColumn {
		if _, err := r.db.ExecContext(ctx, `ALTER TABLE room_outbox ADD COLUMN next_retry_at_ms BIGINT NOT NULL DEFAULT 0 AFTER retry_count`); err != nil {
			return err
		}
	}
	indexes := []struct {
		name      string
		statement string
	}{
		{"idx_room_outbox_pending", `ALTER TABLE room_outbox ADD INDEX idx_room_outbox_pending (app_code, status, next_retry_at_ms, created_at_ms, event_id)`},
		{"idx_room_outbox_claim", `ALTER TABLE room_outbox ADD INDEX idx_room_outbox_claim (app_code, status, lock_until_ms, created_at_ms, event_id)`},
		{"idx_room_outbox_event_created", `ALTER TABLE room_outbox ADD INDEX idx_room_outbox_event_created (app_code, event_type, created_at_ms, event_id)`},
		{"idx_room_outbox_retention", `ALTER TABLE room_outbox ADD INDEX idx_room_outbox_retention (app_code, status, updated_at_ms, event_id)`},
		// 房间维度访问路径：deleteRoomRows 的按房清理 DELETE 和按房排查事件的诊断查询都依赖它；
		// 没有这个索引时两者都要扫整个 app 分片（线上 百万级 行）。
		{"idx_room_outbox_room_event", `ALTER TABLE room_outbox ADD INDEX idx_room_outbox_room_event (app_code, room_id, event_type, created_at_ms, event_id)`},
	}
	for _, index := range indexes {
		hasIndex, err := r.indexExists(ctx, "room_outbox", index.name)
		if err != nil {
			return err
		}
		if !hasIndex {
			if _, err := r.db.ExecContext(ctx, index.statement); err != nil {
				return err
			}
		}
	}
	// 历史版本把 idx_room_outbox_retry 建成了和 idx_room_outbox_pending 完全相同的列组合，
	// 每次写 outbox 都要多维护一份索引；没有任何查询引用它，存在即删除。
	hasLegacyRetryIndex, err := r.indexExists(ctx, "room_outbox", "idx_room_outbox_retry")
	if err != nil {
		return err
	}
	if hasLegacyRetryIndex {
		if _, err := r.db.ExecContext(ctx, `ALTER TABLE room_outbox DROP INDEX idx_room_outbox_retry`); err != nil {
			return err
		}
	}
	return nil
}

func (r *Repository) columnExists(ctx context.Context, tableName string, columnName string) (bool, error) {
	var exists int
	err := r.db.QueryRowContext(ctx, `
		SELECT COUNT(*)
		FROM information_schema.COLUMNS
		WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? AND COLUMN_NAME = ?
	`, tableName, columnName).Scan(&exists)
	return exists > 0, err
}

func (r *Repository) indexExists(ctx context.Context, tableName string, indexName string) (bool, error) {
	var exists int
	err := r.db.QueryRowContext(ctx, `
		SELECT COUNT(*)
		FROM information_schema.STATISTICS
		WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? AND INDEX_NAME = ?
	`, tableName, indexName).Scan(&exists)
	return exists > 0, err
}
