/**
 * 聊天会话管理服务
 *
 * 功能描述：
 * - 维护WebSocket连接ID与Zulip队列ID的映射关系
 * - 管理玩家位置跟踪和上下文注入
 * - 提供空间过滤和会话查询功能
 * - 实现 ISessionManagerService 接口，供其他模块依赖
 *
 * 架构层级：Business Layer（业务层）
 *
 * 最近修改：
 * - 2026-01-14: 代码规范优化 - 提取魔法数字为常量 (修改者: moyin)
 * - 2026-01-14: 代码规范优化 - 补充类级别JSDoc注释 (修改者: moyin)
 * - 2026-01-14: 代码规范优化 - 完善文件头注释和方法注释规范 (修改者: moyin)
 *
 * @author moyin
 * @version 1.1.3
 * @since 2026-01-14
 * @lastModified 2026-01-14
 */

import { Injectable, Logger, Inject } from '@nestjs/common';
import { IRedisService } from '../../../core/redis/redis.interface';
import { IZulipConfigService } from '../../../core/zulip_core/zulip_core.interfaces';
import {
  ISessionManagerService,
  IPosition,
  IGameSession,
  IContextInfo,
  IPlayerAppearance,
  ICafeCompanionPresence,
} from '../../../core/session_core/session_core.interfaces';

// 常量定义
const DEFAULT_MAP_IDS = ['novice_village', 'tavern', 'market'] as const;
const SESSION_TIMEOUT = 3600; // 1小时
const NEARBY_OBJECT_RADIUS = 50; // 附近对象搜索半径

/**
 * 位置信息接口（兼容旧代码）
 */
export type Position = IPosition;

/**
 * 游戏会话接口（兼容旧代码）
 */
export type GameSession = IGameSession;

/**
 * 上下文信息接口（兼容旧代码）
 */
export type ContextInfo = IContextInfo;

/**
 * 聊天好友信息
 */
export interface ChatFriendInfo {
  /** 好友用户ID */
  userId: string;
  /** 好友用户名 */
  username: string;
  /** 当前是否在线 */
  online: boolean;
}

/**
 * 聊天好友请求信息
 */
export interface ChatFriendRequestInfo {
  /** 发起请求的用户ID */
  userId: string;
  /** 发起请求的用户名 */
  username: string;
  /** 请求创建时间 */
  createdAt: string;
}

/**
 * 地图在线玩家信息
 */
export interface MapPlayerPresence {
  /** WebSocket连接ID */
  socketId: string;
  /** 用户ID */
  userId: string;
  /** 用户名 */
  username: string;
  /** 当前地图ID */
  mapId: string;
  /** X坐标 */
  x: number;
  /** Y坐标 */
  y: number;
  /** 外观同步信息 */
  appearance?: IPlayerAppearance;
  /** 咖啡店陪伴服务状态 */
  cafeCompanion?: ICafeCompanionPresence | null;
  /** 是否锁定移动 */
  movementLocked?: boolean;
}

export interface PlayerPresenceMetadata {
  appearance?: IPlayerAppearance;
}

export interface BusinessPresenceUpdate {
  mapId?: string;
  position?: Position;
  cafeCompanion?: ICafeCompanionPresence | null;
  movementLocked?: boolean;
}

/**
 * 聊天会话管理服务类
 *
 * 职责：
 * - 管理WebSocket连接与用户会话的映射
 * - 跟踪玩家在游戏地图中的位置
 * - 根据位置注入聊天上下文（Stream/Topic）
 *
 * 主要方法：
 * - createSession() - 创建新的游戏会话
 * - getSession() - 获取会话信息
 * - updatePlayerPosition() - 更新玩家位置
 * - destroySession() - 销毁会话
 * - injectContext() - 注入聊天上下文
 *
 * 使用场景：
 * - 玩家登录游戏后的会话管理
 * - 基于位置的聊天频道自动切换
 */
@Injectable()
export class ChatSessionService implements ISessionManagerService {
  private readonly SESSION_PREFIX = 'chat:session:';
  private readonly MAP_PLAYERS_PREFIX = 'chat:map_players:';
  private readonly USER_SESSION_PREFIX = 'chat:user_session:';
  private readonly FRIENDS_PREFIX = 'chat:friends:';
  private readonly FRIEND_DATA_PREFIX = 'chat:friend_data:';
  private readonly FRIEND_REQUESTS_PREFIX = 'chat:friend_requests:';
  private readonly FRIEND_REQUEST_DATA_PREFIX = 'chat:friend_request_data:';
  private readonly FRIEND_REQUEST_TIMEOUT = 7 * 24 * 60 * 60; // 7天
  private readonly DEFAULT_MAP = 'novice_village';
  private readonly DEFAULT_POSITION: Position = { x: 400, y: 300 };
  private readonly logger = new Logger(ChatSessionService.name);

  constructor(
    @Inject('REDIS_SERVICE')
    private readonly redisService: IRedisService,
    @Inject('ZULIP_CONFIG_SERVICE')
    private readonly configManager: IZulipConfigService,
  ) {
    this.logger.log('ChatSessionService初始化完成');
  }

  /**
   * 创建会话
   * @param socketId WebSocket连接ID
   * @param userId 用户ID
   * @param zulipQueueId Zulip队列ID
   * @param username 用户名（可选）
   * @param initialMap 初始地图ID（可选）
   * @param initialPosition 初始位置（可选）
   * @returns 创建的游戏会话
   * @throws Error 参数为空时抛出异常
   */
  async createSession(
    socketId: string,
    userId: string,
    zulipQueueId: string,
    username?: string,
    initialMap?: string,
    initialPosition?: Position,
    initialAppearance?: IPlayerAppearance,
  ): Promise<GameSession> {
    this.logger.log('创建游戏会话', { socketId, userId });

    // 参数验证
    if (!socketId?.trim() || !userId?.trim() || !zulipQueueId?.trim()) {
      throw new Error('参数不能为空');
    }

    // 检查并清理旧会话
    const existingSocketId = await this.redisService.get(`${this.USER_SESSION_PREFIX}${userId}`);
    if (existingSocketId) {
      await this.destroySession(existingSocketId);
    }

    // 创建会话对象
    const now = new Date();
    const session: GameSession = {
      socketId,
      userId,
      username: username || `user_${userId}`,
      zulipQueueId,
      currentMap: initialMap || this.DEFAULT_MAP,
      position: initialPosition || { ...this.DEFAULT_POSITION },
      appearance: this.mergeAppearance(undefined, initialAppearance),
      lastActivity: now,
      createdAt: now,
    };

    // 存储到Redis
    const sessionKey = `${this.SESSION_PREFIX}${socketId}`;
    await this.redisService.setex(sessionKey, SESSION_TIMEOUT, this.serializeSession(session));

    // 添加到地图玩家列表
    const mapKey = `${this.MAP_PLAYERS_PREFIX}${session.currentMap}`;
    await this.redisService.sadd(mapKey, socketId);
    await this.redisService.expire(mapKey, SESSION_TIMEOUT);

    // 建立用户到会话的映射
    const userSessionKey = `${this.USER_SESSION_PREFIX}${userId}`;
    await this.redisService.setex(userSessionKey, SESSION_TIMEOUT, socketId);

    this.logger.log('会话创建成功', { socketId, userId, currentMap: session.currentMap });
    return session;
  }

  /**
   * 获取会话信息
   * @param socketId WebSocket连接ID
   * @returns 会话信息或null
   */
  async getSession(socketId: string): Promise<GameSession | null> {
    if (!socketId?.trim()) return null;

    try {
      const sessionKey = `${this.SESSION_PREFIX}${socketId}`;
      const sessionData = await this.redisService.get(sessionKey);
      if (!sessionData) return null;

      const session = this.deserializeSession(sessionData);

      // 更新最后活动时间
      session.lastActivity = new Date();
      await this.redisService.setex(sessionKey, SESSION_TIMEOUT, this.serializeSession(session));

      return session;
    } catch (error) {
      this.logger.error('获取会话失败', { socketId, error: (error as Error).message });
      return null;
    }
  }

  /**
   * 根据用户ID获取当前在线会话的Socket ID
   * @param userId 用户ID
   * @returns 在线Socket ID或null
   */
  async getSocketIdByUserId(userId: string): Promise<string | null> {
    const normalizedUserId = userId?.trim();
    if (!normalizedUserId) return null;

    try {
      const userSessionKey = `${this.USER_SESSION_PREFIX}${normalizedUserId}`;
      const socketId = await this.redisService.get(userSessionKey);
      if (!socketId) return null;

      const session = await this.getSession(socketId);
      if (!session || session.userId !== normalizedUserId) {
        await this.redisService.del(userSessionKey);
        return null;
      }

      return socketId;
    } catch (error) {
      this.logger.error('获取用户Socket失败', { userId: normalizedUserId, error: (error as Error).message });
      return null;
    }
  }

  /**
   * 添加好友
   * @param userId 当前用户ID
   * @param friendUserId 好友用户ID
   * @param friendUsername 好友用户名
   * @returns 好友信息
   */
  async addFriend(userId: string, friendUserId: string, friendUsername?: string): Promise<ChatFriendInfo> {
    const normalizedUserId = userId?.trim();
    const normalizedFriendUserId = friendUserId?.trim();
    if (!normalizedUserId || !normalizedFriendUserId) {
      throw new Error('用户ID不能为空');
    }
    if (normalizedUserId === normalizedFriendUserId) {
      throw new Error('不能添加自己为好友');
    }

    const friend: ChatFriendInfo = {
      userId: normalizedFriendUserId,
      username: friendUsername?.trim() || `user_${normalizedFriendUserId}`,
      online: (await this.getSocketIdByUserId(normalizedFriendUserId)) != null,
    };

    await this.redisService.sadd(this.getFriendsKey(normalizedUserId), normalizedFriendUserId);
    await this.redisService.set(this.getFriendDataKey(normalizedUserId, normalizedFriendUserId), JSON.stringify({
      userId: friend.userId,
      username: friend.username,
    }));

    return friend;
  }

  /**
   * 创建好友请求
   * @param requesterUserId 请求发起人用户ID
   * @param requesterUsername 请求发起人用户名
   * @param targetUserId 目标用户ID
   * @returns 好友请求信息
   */
  async createFriendRequest(
    requesterUserId: string,
    requesterUsername: string,
    targetUserId: string,
  ): Promise<ChatFriendRequestInfo> {
    const normalizedRequesterUserId = requesterUserId?.trim();
    const normalizedTargetUserId = targetUserId?.trim();
    if (!normalizedRequesterUserId || !normalizedTargetUserId) {
      throw new Error('用户ID不能为空');
    }
    if (normalizedRequesterUserId === normalizedTargetUserId) {
      throw new Error('不能添加自己为好友');
    }
    if (await this.isFriend(normalizedRequesterUserId, normalizedTargetUserId)) {
      throw new Error('已经是好友');
    }

    const request: ChatFriendRequestInfo = {
      userId: normalizedRequesterUserId,
      username: requesterUsername?.trim() || `user_${normalizedRequesterUserId}`,
      createdAt: new Date().toISOString(),
    };

    const requestsKey = this.getFriendRequestsKey(normalizedTargetUserId);
    const requestDataKey = this.getFriendRequestDataKey(normalizedTargetUserId, normalizedRequesterUserId);
    await this.redisService.sadd(requestsKey, normalizedRequesterUserId);
    await this.redisService.expire(requestsKey, this.FRIEND_REQUEST_TIMEOUT);
    await this.redisService.setex(requestDataKey, this.FRIEND_REQUEST_TIMEOUT, JSON.stringify(request));

    return request;
  }

  /**
   * 接受好友请求，并建立双向好友关系
   * @param userId 当前用户ID
   * @param requesterUserId 请求发起人用户ID
   * @param currentUsername 当前用户名
   * @returns 双方好友信息
   */
  async acceptFriendRequest(
    userId: string,
    requesterUserId: string,
    currentUsername?: string,
  ): Promise<{ friend: ChatFriendInfo; reciprocalFriend: ChatFriendInfo }> {
    const normalizedUserId = userId?.trim();
    const normalizedRequesterUserId = requesterUserId?.trim();
    if (!normalizedUserId || !normalizedRequesterUserId) {
      throw new Error('用户ID不能为空');
    }

    const request = await this.getFriendRequestData(normalizedUserId, normalizedRequesterUserId);
    if (!request) {
      throw new Error('好友请求不存在或已过期');
    }

    const friend = await this.addFriend(normalizedUserId, normalizedRequesterUserId, request.username);
    const reciprocalFriend = await this.addFriend(
      normalizedRequesterUserId,
      normalizedUserId,
      currentUsername?.trim() || `user_${normalizedUserId}`,
    );
    await this.clearFriendRequest(normalizedUserId, normalizedRequesterUserId);

    return { friend, reciprocalFriend };
  }

  /**
   * 拒绝好友请求
   * @param userId 当前用户ID
   * @param requesterUserId 请求发起人用户ID
   */
  async rejectFriendRequest(userId: string, requesterUserId: string): Promise<void> {
    const normalizedUserId = userId?.trim();
    const normalizedRequesterUserId = requesterUserId?.trim();
    if (!normalizedUserId || !normalizedRequesterUserId) {
      throw new Error('用户ID不能为空');
    }

    await this.clearFriendRequest(normalizedUserId, normalizedRequesterUserId);
  }

  /**
   * 获取收到的好友请求
   * @param userId 当前用户ID
   * @returns 收到的好友请求列表
   */
  async getFriendRequests(userId: string): Promise<ChatFriendRequestInfo[]> {
    const normalizedUserId = userId?.trim();
    if (!normalizedUserId) return [];

    const requesterIds = await this.redisService.smembers(this.getFriendRequestsKey(normalizedUserId));
    const requests: ChatFriendRequestInfo[] = [];

    for (const requesterUserId of requesterIds) {
      const normalizedRequesterUserId = requesterUserId?.trim();
      if (!normalizedRequesterUserId) continue;

      const request = await this.getFriendRequestData(normalizedUserId, normalizedRequesterUserId);
      if (request) {
        requests.push(request);
      }
    }

    return requests.sort((a, b) => b.createdAt.localeCompare(a.createdAt));
  }

  /**
   * 移除好友
   * @param userId 当前用户ID
   * @param friendUserId 好友用户ID
   */
  async removeFriend(userId: string, friendUserId: string): Promise<void> {
    const normalizedUserId = userId?.trim();
    const normalizedFriendUserId = friendUserId?.trim();
    if (!normalizedUserId || !normalizedFriendUserId) {
      throw new Error('用户ID不能为空');
    }

    await this.redisService.srem(this.getFriendsKey(normalizedUserId), normalizedFriendUserId);
    await this.redisService.del(this.getFriendDataKey(normalizedUserId, normalizedFriendUserId));
  }

  /**
   * 获取好友列表
   * @param userId 当前用户ID
   * @returns 好友列表，包含在线状态
   */
  async getFriends(userId: string): Promise<ChatFriendInfo[]> {
    const normalizedUserId = userId?.trim();
    if (!normalizedUserId) return [];

    const friendIds = await this.redisService.smembers(this.getFriendsKey(normalizedUserId));
    const friends: ChatFriendInfo[] = [];

    for (const friendUserId of friendIds) {
      const normalizedFriendUserId = friendUserId?.trim();
      if (!normalizedFriendUserId) continue;

      const data = await this.redisService.get(this.getFriendDataKey(normalizedUserId, normalizedFriendUserId));
      let username = `user_${normalizedFriendUserId}`;
      if (data) {
        try {
          const parsed = JSON.parse(data);
          username = parsed.username || username;
        } catch (error) {
          this.logger.warn('好友数据解析失败', { userId: normalizedUserId, friendUserId: normalizedFriendUserId });
        }
      }

      friends.push({
        userId: normalizedFriendUserId,
        username,
        online: (await this.getSocketIdByUserId(normalizedFriendUserId)) != null,
      });
    }

    return friends.sort((a, b) => {
      if (a.online !== b.online) return a.online ? -1 : 1;
      return a.username.localeCompare(b.username);
    });
  }

  /**
   * 上下文注入：根据位置确定Stream/Topic
   * @param socketId WebSocket连接ID
   * @param mapId 地图ID（可选，默认使用会话当前地图）
   * @returns 上下文信息，包含stream和topic
   */
  async injectContext(socketId: string, mapId?: string): Promise<ContextInfo> {
    try {
      const session = await this.getSession(socketId);
      if (!session) throw new Error('会话不存在');

      const targetMapId = mapId || session.currentMap;
      const stream = this.configManager.getStreamByMap(targetMapId) || 'General';

      let topic = 'General';
      if (session.position) {
        const nearbyObject = this.configManager.findNearbyObject(
          targetMapId,
          session.position.x,
          session.position.y,
          NEARBY_OBJECT_RADIUS
        );
        if (nearbyObject) topic = nearbyObject.zulipTopic;
      }

      return { stream, topic };
    } catch (error) {
      this.logger.error('上下文注入失败', { socketId, error: (error as Error).message });
      return { stream: 'General' };
    }
  }

  /**
   * 获取指定地图的所有Socket
   * @param mapId 地图ID
   * @returns Socket ID列表
   */
  async getSocketsInMap(mapId: string): Promise<string[]> {
    try {
      const mapKey = `${this.MAP_PLAYERS_PREFIX}${mapId}`;
      return await this.redisService.smembers(mapKey);
    } catch (error) {
      this.logger.error('获取地图玩家失败', { mapId, error: (error as Error).message });
      return [];
    }
  }

  /**
   * 获取指定地图内的在线玩家快照
   * @param mapId 地图ID
   * @returns 在线玩家列表
   */
  async getPlayersInMap(mapId: string): Promise<MapPlayerPresence[]> {
    const normalizedMapId = mapId?.trim();
    if (!normalizedMapId) return [];

    const socketIds = await this.getSocketsInMap(normalizedMapId);
    const players: MapPlayerPresence[] = [];

    for (const socketId of socketIds) {
      const sessionKey = `${this.SESSION_PREFIX}${socketId}`;
      const sessionData = await this.redisService.get(sessionKey);
      if (!sessionData) {
        await this.redisService.srem(`${this.MAP_PLAYERS_PREFIX}${normalizedMapId}`, socketId);
        continue;
      }

      const session = this.deserializeSession(sessionData);
      if (session.currentMap !== normalizedMapId) {
        await this.redisService.srem(`${this.MAP_PLAYERS_PREFIX}${normalizedMapId}`, socketId);
        continue;
      }

      const activeSocketId = await this.redisService.get(`${this.USER_SESSION_PREFIX}${session.userId}`);
      if (activeSocketId !== socketId) {
        await this.redisService.srem(`${this.MAP_PLAYERS_PREFIX}${normalizedMapId}`, socketId);
        await this.redisService.del(sessionKey);
        continue;
      }

      players.push({
        socketId: session.socketId,
        userId: session.userId,
        username: session.username,
        mapId: session.currentMap,
        x: Number(session.position?.x ?? 0),
        y: Number(session.position?.y ?? 0),
        appearance: session.appearance,
        cafeCompanion: session.cafeCompanion ?? null,
        movementLocked: Boolean(session.movementLocked),
      });
    }

    return players;
  }

  async updateBusinessPresenceByUserId(
    userId: string,
    update: BusinessPresenceUpdate,
  ): Promise<MapPlayerPresence | null> {
    const normalizedUserId = userId?.trim();
    if (!normalizedUserId) return null;

    const socketId = await this.getSocketIdByUserId(normalizedUserId);
    if (!socketId) return null;

    return this.updateBusinessPresence(socketId, update);
  }

  async updateBusinessPresence(
    socketId: string,
    update: BusinessPresenceUpdate,
  ): Promise<MapPlayerPresence | null> {
    if (!socketId?.trim()) return null;

    try {
      const sessionKey = `${this.SESSION_PREFIX}${socketId}`;
      const sessionData = await this.redisService.get(sessionKey);
      if (!sessionData) return null;

      const session = this.deserializeSession(sessionData);
      const oldMapId = session.currentMap;
      const nextMapId = update.mapId?.trim() || session.currentMap;
      const mapChanged = oldMapId !== nextMapId;

      session.currentMap = nextMapId;
      if (update.position) {
        session.position = { x: update.position.x, y: update.position.y };
      }
      if (update.cafeCompanion !== undefined) {
        session.cafeCompanion = update.cafeCompanion;
      }
      if (update.movementLocked !== undefined) {
        session.movementLocked = update.movementLocked;
      }
      session.lastActivity = new Date();

      await this.redisService.setex(sessionKey, SESSION_TIMEOUT, this.serializeSession(session));

      if (mapChanged) {
        await this.redisService.srem(`${this.MAP_PLAYERS_PREFIX}${oldMapId}`, socketId);
        const newMapKey = `${this.MAP_PLAYERS_PREFIX}${nextMapId}`;
        await this.redisService.sadd(newMapKey, socketId);
        await this.redisService.expire(newMapKey, SESSION_TIMEOUT);
      }

      return {
        socketId: session.socketId,
        userId: session.userId,
        username: session.username,
        mapId: session.currentMap,
        x: Number(session.position?.x ?? 0),
        y: Number(session.position?.y ?? 0),
        appearance: session.appearance,
        cafeCompanion: session.cafeCompanion ?? null,
        movementLocked: Boolean(session.movementLocked),
      };
    } catch (error) {
      this.logger.error('更新玩家业务状态失败', { socketId, error: (error as Error).message });
      return null;
    }
  }

  /**
   * 更新玩家位置
   * @param socketId WebSocket连接ID
   * @param mapId 地图ID
   * @param x X坐标
   * @param y Y坐标
   * @returns 更新是否成功
   */
  async updatePlayerPosition(
    socketId: string,
    mapId: string,
    x: number,
    y: number,
    metadata: PlayerPresenceMetadata = {},
  ): Promise<boolean> {
    if (!socketId?.trim() || !mapId?.trim()) return false;

    try {
      const sessionKey = `${this.SESSION_PREFIX}${socketId}`;
      const sessionData = await this.redisService.get(sessionKey);
      if (!sessionData) return false;

      const session = this.deserializeSession(sessionData);
      const oldMapId = session.currentMap;
      const mapChanged = oldMapId !== mapId;

      // 更新会话
      session.currentMap = mapId;
      if (!session.movementLocked || mapChanged) {
        session.position = { x, y };
      }
      session.appearance = this.mergeAppearance(session.appearance, metadata.appearance);
      if (mapId !== 'whale_cafe') {
        session.cafeCompanion = null;
        session.movementLocked = false;
      }
      session.lastActivity = new Date();
      await this.redisService.setex(sessionKey, SESSION_TIMEOUT, this.serializeSession(session));

      // 如果切换地图，更新地图玩家列表
      if (mapChanged) {
        await this.redisService.srem(`${this.MAP_PLAYERS_PREFIX}${oldMapId}`, socketId);
        const newMapKey = `${this.MAP_PLAYERS_PREFIX}${mapId}`;
        await this.redisService.sadd(newMapKey, socketId);
        await this.redisService.expire(newMapKey, SESSION_TIMEOUT);
      }

      return true;
    } catch (error) {
      this.logger.error('更新位置失败', { socketId, error: (error as Error).message });
      return false;
    }
  }

  /**
   * 销毁会话
   * @param socketId WebSocket连接ID
   * @returns 销毁是否成功
   */
  async destroySession(socketId: string): Promise<boolean> {
    if (!socketId?.trim()) return false;

    try {
      const sessionKey = `${this.SESSION_PREFIX}${socketId}`;
      const sessionData = await this.redisService.get(sessionKey);

      if (!sessionData) return true;

      const session = this.deserializeSession(sessionData);

      // 从地图玩家列表移除
      await this.redisService.srem(`${this.MAP_PLAYERS_PREFIX}${session.currentMap}`, socketId);

      // 旧连接的延迟清理不能删除同账号的新会话映射。
      const userSessionKey = `${this.USER_SESSION_PREFIX}${session.userId}`;
      const activeSocketId = await this.redisService.get(userSessionKey);
      if (activeSocketId === socketId) {
        await this.redisService.del(userSessionKey);
      }

      // 删除会话数据
      await this.redisService.del(sessionKey);

      this.logger.log('会话销毁成功', { socketId, userId: session.userId });
      return true;
    } catch (error) {
      this.logger.error('销毁会话失败', { socketId, error: (error as Error).message });
      return false;
    }
  }

  /**
   * 清理过期会话
   * @param timeoutMinutes 超时时间（分钟），默认30分钟
   * @returns 清理结果，包含清理数量和Zulip队列ID列表
   */
  async cleanupExpiredSessions(timeoutMinutes: number = 30): Promise<{ cleanedCount: number; zulipQueueIds: string[] }> {
    const expiredSessions: GameSession[] = [];
    const zulipQueueIds: string[] = [];
    const timeoutMs = timeoutMinutes * 60 * 1000;
    const now = Date.now();

    try {
      const mapIds = this.configManager.getAllMapIds().length > 0
        ? this.configManager.getAllMapIds()
        : DEFAULT_MAP_IDS;

      for (const mapId of mapIds) {
        const socketIds = await this.getSocketsInMap(mapId);

        for (const socketId of socketIds) {
          const sessionKey = `${this.SESSION_PREFIX}${socketId}`;
          const sessionData = await this.redisService.get(sessionKey);

          if (!sessionData) {
            await this.redisService.srem(`${this.MAP_PLAYERS_PREFIX}${mapId}`, socketId);
            continue;
          }

          const session = this.deserializeSession(sessionData);
          const lastActivityTime = session.lastActivity.getTime();

          if (now - lastActivityTime > timeoutMs) {
            expiredSessions.push(session);
            zulipQueueIds.push(session.zulipQueueId);
          }
        }
      }

      for (const session of expiredSessions) {
        await this.destroySession(session.socketId);
      }

      return { cleanedCount: expiredSessions.length, zulipQueueIds };
    } catch (error) {
      this.logger.error('清理过期会话失败', { error: (error as Error).message });
      return { cleanedCount: 0, zulipQueueIds: [] };
    }
  }

  // ========== 私有方法 ==========

  private serializeSession(session: GameSession): string {
    return JSON.stringify({
      ...session,
      lastActivity: session.lastActivity.toISOString(),
      createdAt: session.createdAt.toISOString(),
    });
  }

  private deserializeSession(data: string): GameSession {
    const parsed = JSON.parse(data);
    return {
      ...parsed,
      lastActivity: new Date(parsed.lastActivity),
      createdAt: new Date(parsed.createdAt),
    };
  }

  private mergeAppearance(
    current: IPlayerAppearance | undefined,
    next: IPlayerAppearance | undefined,
  ): IPlayerAppearance | undefined {
    if (!next) return current;

    const appearance: IPlayerAppearance = { ...(current ?? {}) };
    if (next.skinId !== undefined) {
      appearance.skinId = next.skinId;
      if (next.skinAsset === undefined && current?.skinId !== next.skinId) {
        delete appearance.skinAsset;
      }
    }
    if (next.avatarId !== undefined) {
      appearance.avatarId = next.avatarId;
    }
    if (next.skinAsset !== undefined) {
      appearance.skinAsset = next.skinAsset;
    }

    return appearance;
  }

  private getFriendsKey(userId: string): string {
    return `${this.FRIENDS_PREFIX}${userId}`;
  }

  private getFriendDataKey(userId: string, friendUserId: string): string {
    return `${this.FRIEND_DATA_PREFIX}${userId}:${friendUserId}`;
  }

  private getFriendRequestsKey(userId: string): string {
    return `${this.FRIEND_REQUESTS_PREFIX}${userId}`;
  }

  private getFriendRequestDataKey(userId: string, requesterUserId: string): string {
    return `${this.FRIEND_REQUEST_DATA_PREFIX}${userId}:${requesterUserId}`;
  }

  private async isFriend(userId: string, friendUserId: string): Promise<boolean> {
    const friendIds = await this.redisService.smembers(this.getFriendsKey(userId));
    return friendIds.includes(friendUserId);
  }

  private async getFriendRequestData(userId: string, requesterUserId: string): Promise<ChatFriendRequestInfo | null> {
    const data = await this.redisService.get(this.getFriendRequestDataKey(userId, requesterUserId));
    if (!data) return null;

    try {
      const parsed = JSON.parse(data);
      return {
        userId: parsed.userId || requesterUserId,
        username: parsed.username || `user_${requesterUserId}`,
        createdAt: parsed.createdAt || new Date().toISOString(),
      };
    } catch (error) {
      this.logger.warn('好友请求数据解析失败', { userId, requesterUserId });
      return {
        userId: requesterUserId,
        username: `user_${requesterUserId}`,
        createdAt: new Date().toISOString(),
      };
    }
  }

  private async clearFriendRequest(userId: string, requesterUserId: string): Promise<void> {
    await this.redisService.srem(this.getFriendRequestsKey(userId), requesterUserId);
    await this.redisService.del(this.getFriendRequestDataKey(userId, requesterUserId));
  }
}
