/**
 * 聊天 WebSocket 网关
 *
 * 功能描述：
 * - 处理 WebSocket 协议连接和消息
 * - 只做协议转换，不包含业务逻辑
 * - 将消息路由到 Business 层处理
 *
 * 架构层级：Gateway Layer（网关层）
 *
 * 职责：
 * - WebSocket 连接管理
 * - 消息协议解析
 * - 路由到业务层
 * - 错误转换
 *
 * WebSocket 事件：
 * - connection: 客户端连接事件
 * - message: 消息接收事件（login/logout/chat/position）
 * - close: 客户端断开事件
 * - error: 错误处理事件
 *
 * 最近修改：
 * - 2026-01-14: 代码规范优化 - 提取常量、替换弃用API (修改者: moyin)
 * - 2026-01-14: 代码规范优化 - 完善注释规范 (修改者: moyin)
 *
 * @author moyin
 * @version 1.0.2
 * @since 2026-01-14
 * @lastModified 2026-01-14
 */

import { Injectable, Logger, OnModuleInit, OnModuleDestroy } from '@nestjs/common';
import * as WebSocket from 'ws';
import { ChatService } from '../../business/chat/chat.service';
import { SocialService } from '../../business/social/social.service';
import {
  getTestLabPresence,
  getTestLabPresences,
  removeTestLabPresence,
  TestLabPresence,
  upsertTestLabPresence,
} from '../../business/admin/test_lab_presence.registry';

/** WebSocket 服务器默认端口 */
const DEFAULT_WEBSOCKET_PORT = 3001;

/** 默认地图 ID */
const DEFAULT_MAP_ID = 'whale_port';

/**
 * 扩展的 WebSocket 接口
 */
interface ExtendedWebSocket extends WebSocket {
  id: string;
  isAlive?: boolean;
  authenticated?: boolean;
  userId?: string;
  username?: string;
  sessionId?: string;
  currentMap?: string;
  worldReady?: boolean;
  welcomed?: boolean;
  messageQueue?: Promise<void>;
}

interface MapPositionMessage {
  mapId: string;
  x: number;
  y: number;
}

const WELCOME_RECONNECT_GRACE_MS = 15_000;

/**
 * WebSocket 网关接口 - 供业务层调用
 */
export interface IChatWebSocketGateway {
  sendToPlayer(socketId: string, data: any): void;
  broadcastToMap(mapId: string, data: any, excludeId?: string): void;
  broadcastToAll(data: any, excludeId?: string): void;
  getConnectionCount(): number;
  getAuthenticatedConnectionCount(): number;
  getMapPlayerCounts(): Record<string, number>;
  getMapPlayers(mapId: string): string[];
}

@Injectable()
/**
 * 聊天 WebSocket 网关类
 *
 * 职责：
 * - 管理 WebSocket 客户端连接
 * - 解析和路由 WebSocket 消息
 * - 管理地图房间和玩家广播
 *
 * 主要方法：
 * - sendToPlayer() - 向指定玩家发送消息
 * - broadcastToMap() - 向地图内所有玩家广播
 * - getConnectionCount() - 获取连接数统计
 *
 * 使用场景：
 * - 游戏内实时聊天通信
 * - 玩家位置同步广播
 */
export class ChatWebSocketGateway implements OnModuleInit, OnModuleDestroy, IChatWebSocketGateway {
  private server: WebSocket.Server;
  private readonly logger = new Logger(ChatWebSocketGateway.name);
  private clients = new Map<string, ExtendedWebSocket>();
  private mapRooms = new Map<string, Set<string>>();
  private lastWelcomeAtByUserId = new Map<string, number>();

  constructor(
    private readonly chatService: ChatService,
    private readonly socialService: SocialService,
  ) {}

  async onModuleInit() {
    const port = process.env.WEBSOCKET_PORT ? parseInt(process.env.WEBSOCKET_PORT) : DEFAULT_WEBSOCKET_PORT;

    this.server = new WebSocket.Server({
      port,
      path: '/game'
    });

    this.server.on('connection', (ws: ExtendedWebSocket) => {
      ws.id = this.generateClientId();
      ws.isAlive = true;
      ws.authenticated = false;
      ws.worldReady = false;
      ws.welcomed = false;
      ws.messageQueue = Promise.resolve();

      this.clients.set(ws.id, ws);
      this.logger.log(`新的WebSocket连接: ${ws.id}`);

      ws.on('message', (data) => {
        ws.messageQueue = (ws.messageQueue || Promise.resolve())
          .then(() => this.handleRawMessage(ws, data))
          .catch((error) => this.logger.error(`消息处理失败: ${ws.id}`, error));
      });
      ws.on('close', (code, reason) => this.handleClose(ws, code, reason));
      ws.on('error', (error) => this.handleError(ws, error));

      this.sendMessage(ws, {
        type: 'connected',
        message: '连接成功',
        socketId: ws.id
      });
    });

    // 设置网关引用到业务层
    this.chatService.setWebSocketGateway(this);
    this.socialService.setRealtimeGateway(this);
    this.logger.log(`WebSocket服务器启动成功，端口: ${port}，路径: /game`);
  }

  async onModuleDestroy() {
    if (this.server) {
      this.server.close();
      this.logger.log('WebSocket服务器已关闭');
    }
  }

  /**
   * 处理原始消息 - 协议解析
   *
   * @param ws WebSocket 连接实例
   * @param data 原始消息数据
   */
  private async handleRawMessage(ws: ExtendedWebSocket, data: WebSocket.RawData): Promise<void> {
    try {
      const message = JSON.parse(data.toString());
      await this.routeMessage(ws, message);
    } catch (error) {
      this.logger.error('解析消息失败', error);
      this.sendError(ws, '消息格式错误');
    }
  }

  /**
   * 消息路由 - 根据类型分发到业务层
   *
   * @param ws WebSocket 连接实例
   * @param message 解析后的消息对象
   */
  private async routeMessage(ws: ExtendedWebSocket, message: any) {
    const messageType = message.type || message.t;
    this.logger.log(`收到消息: ${ws.id}, 类型: ${messageType}`);

    switch (messageType) {
      case 'login':
        await this.handleLogin(ws, message);
        break;
      case 'logout':
        await this.handleLogout(ws);
        break;
      case 'chat':
        await this.handleChat(ws, message);
        break;
      case 'dm_send':
        await this.handleDirectMessage(ws, message);
        break;
      case 'dm_read':
        await this.handleDirectMessageRead(ws, message);
        break;
      case 'position':
        await this.handlePosition(ws, message);
        break;
      case 'change_map':
        await this.handleChangeMap(ws, message);
        break;
      case 'world_ready':
        await this.handleWorldReady(ws, message);
        break;
      case 'leave_world':
        await this.handleLeaveWorld(ws, message);
        break;
      case 'appearance_changed':
        await this.handleAppearanceChanged(ws);
        break;
      case 'friend_add':
        await this.handleFriendAdd(ws, message);
        break;
      case 'friend_request':
        await this.handleFriendRequest(ws, message);
        break;
      case 'friend_accept':
        await this.handleFriendAccept(ws, message);
        break;
      case 'friend_reject':
        await this.handleFriendReject(ws, message);
        break;
      case 'friend_remove':
        await this.handleFriendRemove(ws, message);
        break;
      case 'friend_list':
        await this.handleFriendList(ws);
        break;
      default:
        this.logger.warn(`未知消息类型: ${messageType}`);
        this.sendError(ws, `未知消息类型: ${messageType}`);
    }
  }

  /**
   * 处理登录 - 协议转换后调用业务层
   *
   * @param ws WebSocket 连接实例
   * @param message 登录消息（包含 token）
   */
  private async handleLogin(ws: ExtendedWebSocket, message: any) {
    if (!message.token) {
      this.sendError(ws, 'Token不能为空');
      return;
    }

    try {
      const result = await this.chatService.handlePlayerLogin({
        socketId: ws.id,
        token: message.token
      });

      if (result.success) {
        this.disconnectOtherUserConnections(ws.id, String(result.userId || ''));
        ws.authenticated = true;
        ws.userId = result.userId;
        ws.username = result.username;
        ws.sessionId = result.sessionId;
        ws.currentMap = result.currentMap || DEFAULT_MAP_ID;

        this.sendMessage(ws, {
          t: 'login_success',
          sessionId: result.sessionId,
          userId: result.userId,
          username: result.username,
          currentMap: ws.currentMap
        });

        this.logger.log(`用户登录成功: ${result.username} (${ws.id})`);
        await this.socialService.notifyPresenceChanged(String(result.userId), true);
      } else {
        this.sendMessage(ws, {
          t: 'login_error',
          message: result.error || '登录失败'
        });
      }
    } catch (error) {
      this.logger.error('登录处理失败', error);
      this.sendError(ws, '登录处理失败');
    }
  }

  /**
   * 处理登出
   *
   * @param ws WebSocket 连接实例
   */
  private async handleLogout(ws: ExtendedWebSocket) {
    if (!ws.authenticated) {
      this.sendError(ws, '用户未登录');
      return;
    }

    try {
      await this.chatService.handlePlayerLogout(ws.id, 'manual');
      this.cleanupClient(ws);

      this.sendMessage(ws, {
        t: 'logout_success',
        message: '登出成功'
      });

      ws.close(1000, '用户主动登出');
    } catch (error) {
      this.logger.error('登出处理失败', error);
      this.sendError(ws, '登出处理失败');
    }
  }

  /**
   * 处理聊天消息
   *
   * @param ws WebSocket 连接实例
   * @param message 聊天消息（包含 content, scope, mapId）
   */
  private async handleChat(ws: ExtendedWebSocket, message: any) {
    if (!ws.authenticated) {
      this.sendError(ws, '请先登录');
      return;
    }

    if (!message.content) {
      this.sendError(ws, '消息内容不能为空');
      return;
    }

    const scope = String(message.scope || 'local').trim().toLowerCase();
    if (scope === 'private' || scope === 'whisper' || scope === 'dm') {
      await this.handleDirectMessage(ws, message);
      return;
    }

    try {
      const result = await this.chatService.sendChatMessage({
        socketId: ws.id,
        content: message.content,
        scope: message.scope || 'local',
        mapId: message.mapId || ws.currentMap,  // 支持指定目标地图
        targetUserId: message.targetUserId || message.target_user_id,
        targetUsername: message.targetUsername || message.target_username,
        privateContext: message.privateContext || message.private_context,
        bubble: Boolean(message.bubble ?? message.showBubble ?? message.show_bubble),
      });

      if (result.success) {
        this.sendMessage(ws, {
          t: 'chat_sent',
          messageId: result.messageId,
          message: '消息发送成功'
        });
      } else {
        this.sendMessage(ws, {
          t: 'chat_error',
          code: this.toClientErrorCode(result.error),
          message: result.error || '消息发送失败'
        });
      }
    } catch (error) {
      this.logger.error('聊天处理失败', error);
      this.sendError(ws, '聊天处理失败');
    }
  }

  private async handleDirectMessage(ws: ExtendedWebSocket, message: any) {
    if (!ws.authenticated || !ws.userId) {
      this.sendError(ws, '请先登录');
      return;
    }
    const targetUserId = String(message.targetUserId || message.target_user_id || message.userId || message.user_id || '').trim();
    const content = String(message.content || message.txt || '').trim();
    if (!/^\d+$/.test(targetUserId) || !content) {
      this.sendMessage(ws, { t: 'chat_error', code: 'CHAT_ERROR', message: '私聊目标或内容无效' });
      return;
    }
    try {
      const result = await this.socialService.sendDirectMessage(BigInt(ws.userId), BigInt(targetUserId), content);
      this.sendMessage(ws, { t: 'chat_sent', messageId: result.id, message: '消息发送成功' });
    } catch (error) {
      this.sendMessage(ws, { t: 'chat_error', code: this.toClientErrorCode((error as Error).message), message: (error as Error).message || '私聊发送失败' });
    }
  }

  private async handleDirectMessageRead(ws: ExtendedWebSocket, message: any) {
    if (!ws.authenticated || !ws.userId) {
      this.sendError(ws, '请先登录');
      return;
    }
    const targetUserId = String(message.userId || message.user_id || message.targetUserId || '').trim();
    if (!/^\d+$/.test(targetUserId)) {
      this.sendError(ws, '私聊对象无效');
      return;
    }
    try {
      const result = await this.socialService.markConversationRead(BigInt(ws.userId), BigInt(targetUserId));
      this.sendMessage(ws, { t: 'dm_read_success', ...result, userId: targetUserId });
    } catch (error) {
      this.sendError(ws, (error as Error).message || '标记私聊已读失败');
    }
  }

  private async handleFriendAdd(ws: ExtendedWebSocket, message: any) {
    if (!ws.authenticated) {
      this.sendError(ws, '请先登录');
      return;
    }

    const friendUserId = message.friendUserId || message.friend_user_id || message.userId || message.user_id;
    if (!friendUserId) {
      this.sendMessage(ws, { t: 'friend_error', message: '好友用户ID不能为空' });
      return;
    }

    try {
      const request = await this.socialService.createFriendRequest(BigInt(String(ws.userId)), BigInt(String(friendUserId)));
      this.sendMessage(ws, { t: 'friend_request_sent', request });
    } catch (error) {
      this.sendMessage(ws, { t: 'friend_error', code: 'CHAT_ERROR', message: (error as Error).message || '好友请求发送失败' });
    }
  }

  private async handleFriendRequest(ws: ExtendedWebSocket, message: any) {
    if (!ws.authenticated) {
      this.sendError(ws, '请先登录');
      return;
    }

    const friendUserId = message.friendUserId || message.friend_user_id || message.userId || message.user_id;
    if (!friendUserId) {
      this.sendMessage(ws, { t: 'friend_error', message: '好友用户ID不能为空' });
      return;
    }

    try {
      const request = await this.socialService.createFriendRequest(BigInt(String(ws.userId)), BigInt(String(friendUserId)));
      this.sendMessage(ws, { t: 'friend_request_sent', request });
    } catch (error) {
      this.sendMessage(ws, { t: 'friend_error', code: 'CHAT_ERROR', message: (error as Error).message || '好友请求发送失败' });
    }
  }

  private async handleFriendAccept(ws: ExtendedWebSocket, message: any) {
    if (!ws.authenticated) {
      this.sendError(ws, '请先登录');
      return;
    }

    const friendUserId = message.friendUserId || message.friend_user_id || message.userId || message.user_id;
    if (!friendUserId) {
      this.sendMessage(ws, { t: 'friend_error', message: '好友用户ID不能为空' });
      return;
    }

    try {
      const requests = await this.socialService.getFriendRequests(BigInt(String(ws.userId)));
      const request = requests.find((item) => item.requester.id === String(friendUserId));
      if (!request) throw new Error('好友请求不存在或已过期');
      const result = await this.socialService.acceptFriendRequest(BigInt(String(ws.userId)), BigInt(request.id));
      this.sendMessage(ws, { t: 'friend_added', friend: this.legacyFriend(result.friend) });
      await this.sendFriendList(ws);
    } catch (error) {
      this.sendMessage(ws, { t: 'friend_error', code: 'CHAT_ERROR', message: (error as Error).message || '接受好友请求失败' });
    }
  }

  private async handleFriendReject(ws: ExtendedWebSocket, message: any) {
    if (!ws.authenticated) {
      this.sendError(ws, '请先登录');
      return;
    }

    const friendUserId = message.friendUserId || message.friend_user_id || message.userId || message.user_id;
    if (!friendUserId) {
      this.sendMessage(ws, { t: 'friend_error', message: '好友用户ID不能为空' });
      return;
    }

    try {
      const requests = await this.socialService.getFriendRequests(BigInt(String(ws.userId)));
      const request = requests.find((item) => item.requester.id === String(friendUserId));
      if (!request) throw new Error('好友请求不存在或已过期');
      await this.socialService.rejectFriendRequest(BigInt(String(ws.userId)), BigInt(request.id));
      this.sendMessage(ws, { t: 'friend_request_rejected', userId: String(friendUserId) });
      await this.sendFriendList(ws);
    } catch (error) {
      this.sendMessage(ws, { t: 'friend_error', code: 'CHAT_ERROR', message: (error as Error).message || '拒绝好友请求失败' });
    }
  }

  private async handleFriendRemove(ws: ExtendedWebSocket, message: any) {
    if (!ws.authenticated) {
      this.sendError(ws, '请先登录');
      return;
    }

    const friendUserId = message.friendUserId || message.friend_user_id || message.userId || message.user_id;
    if (!friendUserId) {
      this.sendMessage(ws, { t: 'friend_error', message: '好友用户ID不能为空' });
      return;
    }

    try {
      await this.socialService.removeFriend(BigInt(String(ws.userId)), BigInt(String(friendUserId)));
      this.sendMessage(ws, { t: 'friend_removed', friendUserId: String(friendUserId) });
      await this.sendFriendList(ws);
    } catch (error) {
      this.sendMessage(ws, { t: 'friend_error', code: 'CHAT_ERROR', message: (error as Error).message || '移除好友失败' });
    }
  }

  private async handleFriendList(ws: ExtendedWebSocket) {
    if (!ws.authenticated) {
      this.sendError(ws, '请先登录');
      return;
    }

    await this.sendFriendList(ws);
  }

  private async sendFriendList(ws: ExtendedWebSocket) {
    try {
      const userId = BigInt(String(ws.userId));
      const [friends, requests] = await Promise.all([this.socialService.getFriends(userId), this.socialService.getFriendRequests(userId)]);
      this.sendMessage(ws, { t: 'friend_list', friends: friends.map((friend) => this.legacyFriend(friend)), requests: requests.map((request) => ({ userId: request.requester.id, username: request.requester.nickname, createdAt: request.createdAt, requestId: request.id })) });
    } catch (error) {
      this.sendMessage(ws, { t: 'friend_error', code: 'CHAT_ERROR', message: (error as Error).message || '获取好友列表失败' });
    }
  }

  /**
   * 处理位置更新
   *
   * @param ws WebSocket 连接实例
   * @param message 位置消息（包含 x, y, mapId）
   */
  private async handlePosition(ws: ExtendedWebSocket, message: any) {
    if (!ws.authenticated) {
      this.sendError(ws, '请先登录');
      return;
    }
    if (!ws.worldReady) {
      this.sendError(ws, '请先进入世界');
      return;
    }

    const positionMessage = this.normalizePositionMessage(message, ws.currentMap);
    if (!positionMessage) {
      this.sendError(ws, '位置消息无效');
      return;
    }

    try {
      const oldMapId = ws.currentMap || DEFAULT_MAP_ID;
      const mapChanged = oldMapId !== positionMessage.mapId;

      // 如果切换地图，更新房间
      if (mapChanged) {
        this.leaveMapRoom(ws.id, oldMapId);
        this.joinMapRoom(ws.id, positionMessage.mapId);
        ws.currentMap = positionMessage.mapId;
      }

      await this.chatService.updatePlayerPosition({
        socketId: ws.id,
        x: positionMessage.x,
        y: positionMessage.y,
        mapId: positionMessage.mapId,
      });
      const updatedSession = await this.chatService.getSession(ws.id);
      const broadcastX = Number(updatedSession?.position?.x ?? positionMessage.x);
      const broadcastY = Number(updatedSession?.position?.y ?? positionMessage.y);
      const broadcastAppearance = updatedSession?.appearance;

      if (mapChanged) {
        this.broadcastToMap(oldMapId, {
          t: 'player_left',
          userId: ws.userId,
          username: ws.username,
          mapId: oldMapId
        }, ws.id);

        await this.sendMapPlayersSnapshot(ws, positionMessage.mapId);
      }

      const presencePayload = {
        t: 'position_update',
        userId: ws.userId,
        username: ws.username,
        x: broadcastX,
        y: broadcastY,
        mapId: positionMessage.mapId,
        skinId: broadcastAppearance?.skinId,
        avatarId: broadcastAppearance?.avatarId,
        skinAsset: broadcastAppearance?.skinAsset,
        cafeCompanion: updatedSession?.cafeCompanion ?? null,
        movementLocked: Boolean(updatedSession?.movementLocked),
      };

      this.broadcastToMap(positionMessage.mapId, mapChanged ? {
        ...presencePayload,
        t: 'player_joined'
      } : presencePayload, ws.id);

    } catch (error) {
      this.logger.error('位置更新处理失败', error);
      this.sendError(ws, '位置更新处理失败');
    }
  }

  private async handleWorldReady(ws: ExtendedWebSocket, message: any): Promise<void> {
    if (!ws.authenticated) {
      this.sendError(ws, '请先登录');
      return;
    }

    const mapId = String(message.mapId || message.map_id || ws.currentMap || DEFAULT_MAP_ID).trim();
    const x = Number(message.x ?? 400);
    const y = Number(message.y ?? 300);
    if (!mapId || !Number.isFinite(x) || !Number.isFinite(y)) {
      this.sendError(ws, '世界就绪消息无效');
      return;
    }

    const wasWorldReady = Boolean(ws.worldReady);
    const oldMapId = ws.currentMap || DEFAULT_MAP_ID;
    const mapChanged = wasWorldReady && oldMapId !== mapId;

    if (mapChanged) {
      this.broadcastToMap(oldMapId, {
        t: 'player_left',
        userId: ws.userId,
        username: ws.username,
        mapId: oldMapId,
      }, ws.id);
      this.leaveMapRoom(ws.id, oldMapId);
    }

    await this.chatService.updatePlayerPosition({ socketId: ws.id, mapId, x, y });
    const refreshedPresence = await this.chatService.refreshPlayerAppearance(ws.id);
    if (!refreshedPresence) {
      this.sendError(ws, '外观刷新失败');
      return;
    }

    ws.currentMap = mapId;
    ws.worldReady = true;
    this.joinMapRoom(ws.id, mapId);

    this.sendMessage(ws, { t: 'world_ready_success', mapId });
    await this.sendMapPlayersSnapshot(ws, mapId);

    const appearance = refreshedPresence.appearance;
    this.broadcastToMap(mapId, {
      t: 'player_joined',
      userId: ws.userId,
      username: ws.username,
      x: Number(refreshedPresence.x ?? x),
      y: Number(refreshedPresence.y ?? y),
      mapId,
      skinId: appearance?.skinId,
      avatarId: appearance?.avatarId,
      skinAsset: appearance?.skinAsset,
      cafeCompanion: refreshedPresence.cafeCompanion ?? null,
      movementLocked: Boolean(refreshedPresence.movementLocked),
    }, ws.id);

    if (!wasWorldReady && !ws.welcomed) {
      if (this.shouldBroadcastWelcome(String(ws.userId || ''))) {
        this.broadcastToWorldReady({
          t: 'system_presence',
          scope: 'global',
          event: 'player_online',
          userId: ws.userId,
          username: ws.username,
          content: `欢迎 ${ws.username || '新朋友'} 来到鲸鱼镇！`,
          timestamp: new Date().toISOString(),
        });
      }
      ws.welcomed = true;
    }
  }

  private async handleLeaveWorld(ws: ExtendedWebSocket, message: any): Promise<void> {
    if (!ws.authenticated) {
      this.sendError(ws, '请先登录');
      return;
    }

    const oldMapId = ws.currentMap || DEFAULT_MAP_ID;
    if (ws.worldReady) {
      this.broadcastToMap(oldMapId, {
        t: 'player_left',
        userId: ws.userId,
        username: ws.username,
        mapId: oldMapId,
      }, ws.id);
      this.leaveMapRoom(ws.id, oldMapId);
    }

    const sceneId = String(message.sceneId || message.scene_id || 'private_space')
      .trim()
      .toLowerCase()
      .replace(/[^a-z0-9_-]/g, '_');
    const privateMapId = `private:${ws.userId || 'unknown'}:${sceneId || 'private_space'}`;
    const session = await this.chatService.getSession(ws.id);
    await this.chatService.updatePlayerPosition({
      socketId: ws.id,
      mapId: privateMapId,
      x: Number(session?.position?.x ?? 0),
      y: Number(session?.position?.y ?? 0),
    });

    ws.currentMap = privateMapId;
    ws.worldReady = false;
    this.sendMessage(ws, { t: 'world_left', mapId: oldMapId, sceneId });
  }

  private async handleAppearanceChanged(ws: ExtendedWebSocket): Promise<void> {
    if (!ws.authenticated || !ws.worldReady) {
      this.sendError(ws, '请先进入世界');
      return;
    }

    const presence = await this.chatService.refreshPlayerAppearance(ws.id);
    if (!presence) {
      this.sendError(ws, '外观刷新失败');
      return;
    }

    this.broadcastToMap(presence.mapId, {
      t: 'appearance_changed',
      ...presence,
    }, ws.id);
  }

  /**
   * 处理切换地图
   *
   * @param ws WebSocket 连接实例
   * @param message 切换地图消息（包含 mapId）
   */
  private async handleChangeMap(ws: ExtendedWebSocket, message: any) {
    if (!ws.authenticated) {
      this.sendError(ws, '请先登录');
      return;
    }

    if (!message.mapId) {
      this.sendError(ws, '地图ID不能为空');
      return;
    }

    try {
      const oldMapId = ws.currentMap;
      const newMapId = message.mapId;
      const x = Number(message.x ?? 400);
      const y = Number(message.y ?? 300);

      // 如果地图相同，直接返回成功
      if (oldMapId === newMapId) {
        this.sendMessage(ws, {
          t: 'map_changed',
          mapId: newMapId,
          message: '已在当前地图'
        });
        return;
      }

      // 更新房间
      this.leaveMapRoom(ws.id, oldMapId);
      this.joinMapRoom(ws.id, newMapId);
      ws.currentMap = newMapId;

      // 更新会话中的地图信息（使用默认位置）
      await this.chatService.updatePlayerPosition({
        socketId: ws.id,
        x: Number.isFinite(x) ? x : 400,
        y: Number.isFinite(y) ? y : 300,
        mapId: newMapId,
      });
      const updatedSession = await this.chatService.getSession(ws.id);

      // 通知客户端切换成功
      this.sendMessage(ws, {
        t: 'map_changed',
        mapId: newMapId,
        oldMapId: oldMapId,
        message: '地图切换成功'
      });

      // 向旧地图广播玩家离开
      this.broadcastToMap(oldMapId, {
        t: 'player_left',
        userId: ws.userId,
        username: ws.username,
        mapId: oldMapId
      });

      // 向新地图广播玩家加入
      this.broadcastToMap(newMapId, {
        t: 'player_joined',
        userId: ws.userId,
        username: ws.username,
        x: Number.isFinite(x) ? x : 400,
        y: Number.isFinite(y) ? y : 300,
        mapId: newMapId,
        skinId: updatedSession?.appearance?.skinId,
        avatarId: updatedSession?.appearance?.avatarId,
        skinAsset: updatedSession?.appearance?.skinAsset,
      }, ws.id);

      await this.sendMapPlayersSnapshot(ws, newMapId);
      this.logger.log(`用户切换地图: ${ws.username} (${oldMapId} -> ${newMapId})`);

    } catch (error) {
      this.logger.error('切换地图处理失败', error);
      this.sendError(ws, '切换地图处理失败');
    }
  }

  /**
   * 处理连接关闭
   *
   * @param ws WebSocket 连接实例
   * @param code 关闭状态码
   * @param reason 关闭原因
   */
  private handleClose(ws: ExtendedWebSocket, code: number, reason: Buffer) {
    this.logger.log(`WebSocket连接关闭: ${ws.id}`, { code, reason: reason?.toString() });

    let logoutReason: 'manual' | 'timeout' | 'disconnect' = 'disconnect';
    if (code === 1000) logoutReason = 'manual';

    this.cleanupClient(ws, logoutReason);
  }

  /**
   * 处理错误
   *
   * @param ws WebSocket 连接实例
   * @param error 错误对象
   */
  private handleError(ws: ExtendedWebSocket, error: Error) {
    this.logger.error(`WebSocket错误: ${ws.id}`, error);
  }

  // ========== IChatWebSocketGateway 接口实现 ==========

  public sendToPlayer(socketId: string, data: any): void {
    const client = this.clients.get(socketId);
    if (client && client.readyState === WebSocket.OPEN) {
      this.sendMessage(client, data);
    }
  }

  public async broadcastToMap(mapId: string, data: any, excludeId?: string): Promise<void> {
    const room = this.mapRooms.get(mapId);
    if (!room) return;

    for (const clientId of room) {
      if (clientId !== excludeId) {
        const client = this.clients.get(clientId);
        if (client && client.authenticated && client.readyState === WebSocket.OPEN) {
          if (data?.t === 'chat_render' && data?.fromUserId && client.userId && !(await this.socialService.canSeeChat(String(data.fromUserId), String(client.userId)))) continue;
          this.sendMessage(client, data);
        }
      }
    }
  }

  public async broadcastToAll(data: any, excludeId?: string): Promise<void> {
    for (const [clientId, client] of this.clients) {
      if (clientId === excludeId) continue;
      if (client.authenticated && client.readyState === WebSocket.OPEN) {
        if (data?.t === 'chat_render' && data?.fromUserId && client.userId && !(await this.socialService.canSeeChat(String(data.fromUserId), String(client.userId)))) continue;
        this.sendMessage(client, data);
      }
    }
  }

  public getConnectionCount(): number {
    return this.clients.size;
  }

  public getAuthenticatedConnectionCount(): number {
    return Array.from(this.clients.values()).filter(c => c.authenticated).length;
  }

  public getMapPlayerCounts(): Record<string, number> {
    const counts: Record<string, number> = {};
    this.mapRooms.forEach((clients, mapId) => {
      counts[mapId] = clients.size;
    });
    return counts;
  }

  public getMapPlayers(mapId: string): string[] {
    const room = this.mapRooms.get(mapId);
    if (!room) return [];

    const players: string[] = [];
    room.forEach(clientId => {
      const client = this.clients.get(clientId);
      if (client?.authenticated && client.username) {
        players.push(client.username);
      }
    });
    return players;
  }

  /** 管理端测试实验室使用：返回真实在线玩家，绝不包含合成假人。 */
  public getOnlineWorldPlayers(): Array<{ userId: string; username: string; mapId: string }> {
    return [...this.clients.values()]
      .filter((client) => client.authenticated && client.worldReady && client.userId && client.currentMap)
      .map((client) => ({ userId: String(client.userId), username: String(client.username || ''), mapId: String(client.currentMap) }));
  }

  public setTestLabPresence(presence: TestLabPresence): void {
    const previous = getTestLabPresence(presence.userId);
    const current = upsertTestLabPresence(presence);
    if (previous?.online && (!current.online || previous.mapId !== current.mapId)) {
      this.broadcastToMap(previous.mapId, { t: 'player_left', userId: previous.userId, username: previous.nickname, mapId: previous.mapId });
    }
    if (!current.online) return;
    const payload = {
      t: previous?.online && previous.mapId === current.mapId ? 'position_update' : 'player_joined',
      userId: current.userId,
      username: current.nickname,
      mapId: current.mapId,
      x: current.x,
      y: current.y,
      skinId: current.skinId,
      avatarId: current.avatarId,
      appearance: { skinId: current.skinId, avatarId: current.avatarId },
    };
    this.broadcastToMap(current.mapId, payload);
  }

  public removeTestLabActor(userId: string): void {
    const previous = removeTestLabPresence(userId);
    if (previous?.online) {
      this.broadcastToMap(previous.mapId, { t: 'player_left', userId: previous.userId, username: previous.nickname, mapId: previous.mapId });
    }
  }

  public broadcastTestLabChat(actor: TestLabPresence, content: string, scope: 'local' | 'global' = 'local'): void {
    const payload = {
      t: 'chat_render',
      from: actor.nickname,
      fromUserId: actor.userId,
      txt: content,
      bubble: true,
      timestamp: new Date().toISOString(),
      messageId: `test_${Date.now()}_${actor.userId}`,
      mapId: actor.mapId,
      scope,
      testLab: true,
    };
    if (scope === 'global') this.broadcastToAll(payload);
    else this.broadcastToMap(actor.mapId, payload);
  }

  // ========== 私有辅助方法 ==========

  private sendMessage(ws: ExtendedWebSocket, data: any) {
    if (ws.readyState === WebSocket.OPEN) {
      ws.send(JSON.stringify(data));
    }
  }

  private sendError(ws: ExtendedWebSocket, message: string) {
    this.sendMessage(ws, { type: 'error', code: this.toClientErrorCode(message), message });
  }

  private toClientErrorCode(message?: string): string {
    const normalizedMessage = String(message || '');
    if (normalizedMessage.includes('会话不存在') || normalizedMessage.includes('重新登录')) {
      return 'SESSION_EXPIRED';
    }
    if (normalizedMessage.includes('请先登录') || normalizedMessage.includes('Token')) {
      return 'AUTH_FAILED';
    }
    return 'CHAT_ERROR';
  }

  private joinMapRoom(clientId: string, mapId: string) {
    if (!this.mapRooms.has(mapId)) {
      this.mapRooms.set(mapId, new Set());
    }
    this.mapRooms.get(mapId).add(clientId);
  }

  private leaveMapRoom(clientId: string, mapId: string) {
    const room = this.mapRooms.get(mapId);
    if (room) {
      room.delete(clientId);
      if (room.size === 0) this.mapRooms.delete(mapId);
    }
  }

  private normalizePositionMessage(message: any, currentMap?: string): MapPositionMessage | null {
    const mapId = String(message.mapId || message.map_id || currentMap || DEFAULT_MAP_ID).trim();
    const x = Number(message.x);
    const y = Number(message.y);

    if (!mapId || !Number.isFinite(x) || !Number.isFinite(y)) {
      return null;
    }

    return {
      mapId,
      x,
      y,
    };
  }

  private async sendMapPlayersSnapshot(ws: ExtendedWebSocket, mapId?: string): Promise<void> {
    const normalizedMapId = String(mapId || DEFAULT_MAP_ID).trim();
    if (!normalizedMapId) return;

    const activeUserIds = new Set<string>();
    this.mapRooms.get(normalizedMapId)?.forEach((socketId) => {
      const client = this.clients.get(socketId);
      if (client?.worldReady && client.userId) activeUserIds.add(String(client.userId));
    });
    const players = (await this.chatService.getMapPlayerSnapshot(normalizedMapId, ws.id))
      .filter((player) => activeUserIds.has(String(player.userId)));
    const testPlayers = getTestLabPresences(normalizedMapId).map((player) => ({
      userId: player.userId,
      username: player.nickname,
      mapId: player.mapId,
      x: player.x,
      y: player.y,
      skinId: player.skinId,
      avatarId: player.avatarId,
      appearance: { skinId: player.skinId, avatarId: player.avatarId },
      cafeCompanion: null,
      movementLocked: false,
    }));
    this.sendMessage(ws, {
      t: 'map_players_snapshot',
      mapId: normalizedMapId,
      players: [...players, ...testPlayers].filter((player) => String(player.userId) !== String(ws.userId)),
    });
  }

  private async cleanupClient(ws: ExtendedWebSocket, reason: 'manual' | 'timeout' | 'disconnect' = 'disconnect') {
    try {
      if (ws.authenticated && ws.worldReady && ws.currentMap) {
        this.broadcastToMap(ws.currentMap, {
          t: 'player_left',
          userId: ws.userId,
          username: ws.username,
          mapId: ws.currentMap,
        }, ws.id);
      }
      if (ws.authenticated && ws.id) {
        await this.chatService.handlePlayerLogout(ws.id, reason);
        if (ws.userId) await this.socialService.notifyPresenceChanged(String(ws.userId), false);
      }
      if (ws.currentMap) {
        this.leaveMapRoom(ws.id, ws.currentMap);
      }
      this.clients.delete(ws.id);
    } catch (error) {
      this.logger.error(`清理客户端失败: ${ws.id}`, error);
    }
  }

  private broadcastToWorldReady(data: any): void {
    this.clients.forEach((client) => {
      if (client.authenticated && client.worldReady && client.readyState === WebSocket.OPEN) {
        this.sendMessage(client, data);
      }
    });
  }

  private shouldBroadcastWelcome(userId: string): boolean {
    if (!userId) return false;
    const now = Date.now();
    const lastWelcomeAt = this.lastWelcomeAtByUserId.get(userId) || 0;
    if (now - lastWelcomeAt < WELCOME_RECONNECT_GRACE_MS) return false;
    this.lastWelcomeAtByUserId.set(userId, now);
    return true;
  }

  private disconnectOtherUserConnections(currentSocketId: string, userId: string): void {
    if (!userId) return;
    this.clients.forEach((client, socketId) => {
      if (socketId !== currentSocketId && String(client.userId || '') === userId) {
        client.close(4001, '账号已在新连接中登录');
      }
    });
  }

  private generateClientId(): string {
    return `ws_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`;
  }

  private legacyFriend(profile: any) {
    return {
      userId: String(profile.id),
      username: String(profile.nickname || profile.username || '玩家'),
      online: Boolean(profile.online),
      room_visitable: Boolean(profile.room_visitable),
    };
  }
}
