import {
  BadRequestException,
  ForbiddenException,
  Inject,
  Injectable,
  Logger,
  NotFoundException,
} from '@nestjs/common';
import { Cron, CronExpression } from '@nestjs/schedule';
import { AccountProfileService } from '../auth/account_profile.service';
import { getTestLabPresence } from '../admin/test_lab_presence.registry';
import { ChatSessionService } from '../chat/services/chat_session.service';
import { FriendRequestStatus } from './social.entities';
import { SOCIAL_STORE, SocialStore } from './social.store';

const NEARBY_DISTANCE = 160;
const RETENTION_MS = 30 * 24 * 60 * 60 * 1000;
const FRIEND_REQUEST_MS = 30 * 24 * 60 * 60 * 1000;
const NICKNAME_COOLDOWN_MS = 7 * 24 * 60 * 60 * 1000;

export const INTEREST_TAGS = [
  { id: 'ai', label: 'AI/大模型' },
  { id: 'programming', label: '编程开发' },
  { id: 'data_science', label: '数据科学' },
  { id: 'open_source', label: '开源协作' },
  { id: 'product', label: '产品' },
  { id: 'design', label: '设计' },
  { id: 'game_dev', label: '游戏开发' },
  { id: 'content_creation', label: '内容创作' },
  { id: 'community', label: '社区活动' },
  { id: 'learning_partner', label: '学习搭子' },
  { id: 'career', label: '职业成长' },
  { id: 'casual_chat', label: '轻松闲聊' },
] as const;

const INTEREST_IDS = new Set<string>(INTEREST_TAGS.map((tag) => tag.id));
const SOCIAL_SETTINGS_KEY = 'whaletown_settings';
const DEFAULT_PRIVACY = {
  allow_nearby_profile: true,
  allow_nearby_private: true,
  allow_nearby_friend_requests: true,
};
const ROOM_VISIT_POLICIES = new Set(['friends', 'public', 'closed']);
const TRAVEL_MAP_ORIGINS: Record<string, { x: number; y: number }> = {
  whale_port: { x: 1280, y: 960 },
  work_zone: { x: 1280, y: 960 },
  whale_cafe: { x: 768, y: 512 },
  personal_space: { x: 768, y: 512 },
};
const DISCOVERY_DISTANCE = 180;

export const TRAVEL_DESTINATIONS = [
  { id: 'square_center', mapId: 'whale_port', label: '广场中心', x: 1280, y: 990, initial: true },
  { id: 'square_dock', mapId: 'whale_port', label: '码头', x: 410, y: 758 },
  { id: 'square_headquarters', mapId: 'whale_port', label: '总部', x: 1293, y: 360 },
  { id: 'square_cottage', mapId: 'whale_port', label: '小屋', x: 2125, y: 768 },
  { id: 'square_workshop', mapId: 'whale_port', label: '工坊', x: 1925, y: 1460 },
  { id: 'square_notice', mapId: 'whale_port', label: '公告栏', x: 738, y: 1608 },
  { id: 'square_work_zone_gate', mapId: 'whale_port', label: '打工区入口', x: 1280, y: 1735 },
  { id: 'work_entrance', mapId: 'work_zone', label: '打工区入口', x: 1280, y: 1715 },
  { id: 'work_mall', mapId: 'work_zone', label: '商城', x: 1280, y: 346 },
  { id: 'work_cafe_gate', mapId: 'work_zone', label: '咖啡馆入口', x: 236, y: 1182 },
  { id: 'work_jobs', mapId: 'work_zone', label: '任务中心', x: 778, y: 1152 },
  { id: 'work_courses', mapId: 'work_zone', label: '课程看板', x: 1776, y: 960 },
  { id: 'work_ai', mapId: 'work_zone', label: 'AI 站', x: 1732, y: 1508 },
  { id: 'work_exchange', mapId: 'work_zone', label: '鲸币兑换处', x: 2355, y: 1508 },
  { id: 'cafe_entrance', mapId: 'whale_cafe', label: '咖啡馆入口', x: 768, y: 875 },
  { id: 'cafe_counter', mapId: 'whale_cafe', label: '服务台', x: 768, y: 475 },
  { id: 'cafe_companion', mapId: 'whale_cafe', label: '陪伴区', x: 370, y: 286 },
  { id: 'personal_room', mapId: 'personal_space', label: '我的房间', x: 768, y: 512, initial: true },
] as const;

type RealtimeGateway = {
  sendToPlayer(socketId: string, payload: Record<string, unknown>): void;
};

interface UserRecord {
  id: bigint;
  username: string;
  nickname: string;
  avatar_url?: string | null;
  nickname_updated_at?: Date | null;
}

interface UserService {
  findOne(id: bigint): Promise<UserRecord>;
  update(id: bigint, payload: Record<string, unknown>): Promise<UserRecord>;
}

interface ProfileRecord {
  id: bigint;
  user_id: bigint;
  bio?: string | null;
  tags?: Record<string, unknown> | null;
  skin_id?: string | null;
  current_map: string;
  pos_x: number;
  pos_y: number;
}

interface ProfileService {
  findByUserId(userId: bigint): Promise<ProfileRecord | null>;
  update(id: bigint, payload: Record<string, unknown>): Promise<ProfileRecord>;
}

@Injectable()
export class SocialService {
  private readonly logger = new Logger(SocialService.name);
  private realtimeGateway?: RealtimeGateway;

  constructor(
    @Inject(SOCIAL_STORE) private readonly store: SocialStore,
    @Inject('UsersService') private readonly usersService: UserService,
    @Inject('IUserProfilesService') private readonly profiles: ProfileService,
    private readonly accountProfileService: AccountProfileService,
    private readonly sessions: ChatSessionService,
  ) {}

  setRealtimeGateway(gateway: RealtimeGateway): void {
    this.realtimeGateway = gateway;
  }

  getInterestTags() { return INTEREST_TAGS; }

  async getOwnSocialProfile(userId: bigint) {
    await this.ensureProfile(userId);
    return this.buildProfile(userId, userId, true);
  }

  async updateSocialProfile(userId: bigint, update: { nickname?: string; bio?: string; interests?: string[] }) {
    const user = await this.usersService.findOne(userId);
    const profile = await this.ensureProfile(userId);

    if (update.nickname !== undefined) {
      const nickname = update.nickname.trim();
      if (!nickname) throw new BadRequestException('昵称不能为空');
      if (nickname !== user.nickname) {
        const lastUpdatedAt = user.nickname_updated_at ? new Date(user.nickname_updated_at).getTime() : 0;
        const remaining = NICKNAME_COOLDOWN_MS - (Date.now() - lastUpdatedAt);
        if (lastUpdatedAt && remaining > 0) {
          throw new ForbiddenException(`昵称每 7 天只能修改一次，还需等待 ${Math.ceil(remaining / 86400000)} 天`);
        }
        await this.usersService.update(userId, { nickname, nickname_updated_at: new Date() });
      }
    }

    const tags = this.profileTags(profile);
    if (update.interests !== undefined) {
      const interests = [...new Set(update.interests.map((value) => value.trim()))];
      if (interests.length > 3 || interests.some((value) => !INTEREST_IDS.has(value))) {
        throw new BadRequestException('兴趣标签不在允许的目录内');
      }
      tags.interests = interests;
    }
    await this.profiles.update(profile.id, { bio: update.bio !== undefined ? update.bio.trim() : profile.bio || '', tags });
    return this.getOwnSocialProfile(userId);
  }

  async getPublicProfile(viewerId: bigint, targetId: bigint) {
    const self = viewerId === targetId;
    if (!self && !(await this.areFriends(viewerId, targetId))) {
      await this.assertNearbyAllowed(viewerId, targetId, 'profile');
    }
    return this.buildProfile(viewerId, targetId, self);
  }

  async canVisitRoom(viewerId: bigint, ownerId: bigint): Promise<boolean> {
    if (viewerId === ownerId) return true;
    const profile = await this.ensureProfile(ownerId);
    return await this.canVisitRoomWithProfile(viewerId, ownerId, profile);
  }

  async getRoomOwnerProfile(viewerId: bigint, ownerId: bigint) {
    if (!(await this.canVisitRoom(viewerId, ownerId))) {
      throw new ForbiddenException('该玩家的个人空间暂不可访问');
    }
    const profile = await this.buildProfile(viewerId, ownerId, viewerId === ownerId);
    return {
      id: profile.id,
      username: profile.username,
      nickname: profile.nickname,
      avatarUrl: profile.avatarUrl,
      skinId: profile.skinId,
    };
  }

  async getFriends(userId: bigint) {
    const friendships = await this.store.listFriendships(userId);
    const result = [];
    for (const friendship of friendships) {
      const friendId = friendship.user_low_id === userId ? friendship.user_high_id : friendship.user_low_id;
      result.push(await this.buildProfile(userId, friendId, false));
    }
    return result.sort((a, b) => Number(b.online) - Number(a.online) || a.nickname.localeCompare(b.nickname));
  }

  async getFriendRequests(userId: bigint) {
    const requests = await this.store.listFriendRequests(userId);
    return Promise.all(requests.map(async (request) => ({
      id: request.id.toString(),
      createdAt: request.created_at,
      expiresAt: request.expires_at,
      requester: await this.buildProfile(userId, request.requester_id, false),
    })));
  }

  async createFriendRequest(requesterId: bigint, targetId: bigint) {
    this.assertDistinct(requesterId, targetId);
    await this.assertNotBlockedEitherWay(requesterId, targetId);
    if (await this.areFriends(requesterId, targetId)) throw new BadRequestException('已经是好友');
    await this.assertNearbyAllowed(requesterId, targetId, 'friend');
    if (await this.store.findPendingFriendRequest(requesterId, targetId)) throw new BadRequestException('好友请求已发送');

    await this.usersService.findOne(targetId);
    const request = await this.store.createFriendRequest(requesterId, targetId, new Date(Date.now() + FRIEND_REQUEST_MS));
    const requester = await this.buildProfile(targetId, requesterId, false);
    await this.createNotification(targetId, 'friend_request', '新的好友申请', `${requester.nickname} 想与你成为好友`, { requestId: request.id.toString(), userId: requesterId.toString() });
    await this.sendToUser(targetId, { t: 'friend_request_received', request: { id: request.id.toString(), requester } });
    return { id: request.id.toString(), createdAt: request.created_at, expiresAt: request.expires_at };
  }

  /** 仅供测试实验室调用：使用真实社交记录，但合成玩家无需真实 WebSocket 会话。 */
  async createTestFriendRequest(requesterId: bigint, targetId: bigint) {
    this.assertDistinct(requesterId, targetId);
    await this.assertNotBlockedEitherWay(requesterId, targetId);
    if (await this.areFriends(requesterId, targetId)) throw new BadRequestException('已经是好友');
    if (await this.store.findPendingFriendRequest(requesterId, targetId)) throw new BadRequestException('好友请求已发送');

    await this.usersService.findOne(targetId);
    const request = await this.store.createFriendRequest(requesterId, targetId, new Date(Date.now() + FRIEND_REQUEST_MS));
    const requester = await this.buildProfile(targetId, requesterId, false);
    await this.createNotification(targetId, 'friend_request', '新的好友申请', `${requester.nickname} 想与你成为好友`, { requestId: request.id.toString(), userId: requesterId.toString(), testLab: true });
    await this.sendToUser(targetId, { t: 'friend_request_received', request: { id: request.id.toString(), requester } });
    return { id: request.id.toString(), createdAt: request.created_at, expiresAt: request.expires_at };
  }

  async respondToTestFriendRequest(actorId: bigint, requesterId: bigint, accept: boolean) {
    const request = await this.store.findPendingFriendRequest(requesterId, actorId);
    if (!request) throw new NotFoundException('不存在待处理的好友申请');
    return accept
      ? this.acceptFriendRequest(actorId, request.id)
      : this.rejectFriendRequest(actorId, request.id);
  }

  async acceptFriendRequest(userId: bigint, requestId: bigint) {
    const request = await this.store.findFriendRequest(requestId);
    if (!request || request.recipient_id !== userId || request.status !== FriendRequestStatus.PENDING || request.expires_at <= new Date()) throw new NotFoundException('好友请求不存在或已过期');
    await this.assertNotBlockedEitherWay(userId, request.requester_id);
    const [low, high] = this.sortIds(userId, request.requester_id);
    await this.store.createFriendship(low, high);
    request.status = FriendRequestStatus.ACCEPTED;
    request.responded_at = new Date();
    await this.store.saveFriendRequest(request);
    await this.store.cancelPendingRequestsBetween(userId, request.requester_id);
    const accepter = await this.buildProfile(request.requester_id, userId, false);
    await this.createNotification(request.requester_id, 'friend_accepted', '好友申请已接受', `${accepter.nickname} 已成为你的好友`, { userId: userId.toString() });
    await this.sendToUser(request.requester_id, { t: 'friendship_changed', action: 'accepted', friend: accepter });
    await this.sendToUser(userId, { t: 'friendship_changed', action: 'accepted', friend: await this.buildProfile(userId, request.requester_id, false) });
    return { friend: await this.buildProfile(userId, request.requester_id, false) };
  }

  async rejectFriendRequest(userId: bigint, requestId: bigint) {
    const request = await this.store.findFriendRequest(requestId);
    if (!request || request.recipient_id !== userId || request.status !== FriendRequestStatus.PENDING) throw new NotFoundException('好友请求不存在');
    request.status = FriendRequestStatus.REJECTED;
    request.responded_at = new Date();
    await this.store.saveFriendRequest(request);
    const rejecter = await this.buildProfile(request.requester_id, userId, false);
    await this.createNotification(request.requester_id, 'friend_rejected', '好友申请未通过', `${rejecter.nickname} 暂未接受你的好友申请`, { userId: userId.toString() });
    await this.sendToUser(request.requester_id, { t: 'friendship_changed', action: 'rejected', userId: userId.toString() });
  }

  async cancelFriendRequest(userId: bigint, requestId: bigint) {
    const request = await this.store.findFriendRequest(requestId);
    if (!request || request.requester_id !== userId || request.status !== FriendRequestStatus.PENDING) throw new NotFoundException('好友请求不存在');
    request.status = FriendRequestStatus.CANCELLED;
    request.responded_at = new Date();
    await this.store.saveFriendRequest(request);
  }

  async removeFriend(userId: bigint, friendId: bigint) {
    const [low, high] = this.sortIds(userId, friendId);
    await this.store.deleteFriendship(low, high);
    await this.sendToUser(friendId, { t: 'friendship_changed', action: 'removed', userId: userId.toString() });
  }

  async listBlocks(userId: bigint) {
    const blocks = await this.store.listBlocks(userId);
    return Promise.all(blocks.map(async (block) => ({ createdAt: block.created_at, profile: await this.buildProfile(userId, block.blocked_user_id, false) })));
  }

  async blockUser(userId: bigint, targetId: bigint) {
    this.assertDistinct(userId, targetId);
    await this.usersService.findOne(targetId);
    await this.store.createBlock(userId, targetId);
    const [low, high] = this.sortIds(userId, targetId);
    await this.store.deleteFriendship(low, high);
    await this.store.cancelPendingRequestsBetween(userId, targetId);
    await this.sendToUser(targetId, { t: 'friendship_changed', action: 'removed', userId: userId.toString() });
    return { success: true };
  }

  async unblockUser(userId: bigint, targetId: bigint) {
    await this.store.deleteBlock(userId, targetId);
    return { success: true };
  }

  async createReport(reporterId: bigint, input: { userId: bigint; reason: string; note?: string; messageId?: bigint; blockAlso?: boolean }) {
    this.assertDistinct(reporterId, input.userId);
    const report = await this.store.createReport({ reporter_id: reporterId, reported_user_id: input.userId, reason: input.reason, note: input.note?.trim() || null, message_id: input.messageId || null });
    if (input.blockAlso) await this.blockUser(reporterId, input.userId);
    await this.createNotification(reporterId, 'report_receipt', '举报已提交', '我们已收到你的举报，会尽快处理。', { reportId: report.id.toString() });
    return { id: report.id.toString(), status: report.status };
  }

  async sendDirectMessage(senderId: bigint, targetId: bigint, content: string) {
    this.assertDistinct(senderId, targetId);
    const normalizedContent = content.trim();
    if (!normalizedContent || normalizedContent.length > 1000) throw new BadRequestException('私聊内容需为 1-1000 个字符');
    await this.assertNotBlockedEitherWay(senderId, targetId);
    const targetSocket = await this.sessions.getSocketIdByUserId(targetId.toString());
    const targetTestPresence = getTestLabPresence(targetId.toString());
    // 测试实验室假人没有真实 WebSocket 连接，但其合成在线状态应与普通在线玩家
    // 一致地通过私聊在线校验。消息仍只写入现有的私聊存储，不触发外部副作用。
    if (!targetSocket && !targetTestPresence?.online) throw new BadRequestException('对方当前不在线');
    if (!(await this.areFriends(senderId, targetId))) await this.assertNearbyAllowed(senderId, targetId, 'private');

    const message = await this.store.createDirectMessage({ sender_id: senderId, recipient_id: targetId, content: normalizedContent, expires_at: new Date(Date.now() + RETENTION_MS) });
    const sender = await this.buildProfile(targetId, senderId, false);
    const payload = { t: 'dm_message', message: { id: message.id.toString(), senderId: senderId.toString(), recipientId: targetId.toString(), content: message.content, createdAt: message.created_at, sender } };
    await this.sendToUser(senderId, payload);
    await this.sendToUser(targetId, payload);
    return payload.message;
  }

  /** 测试假人私聊：仍写入正式私聊存储，但不要求假人拥有真实 socket。 */
  async sendTestDirectMessage(senderId: bigint, targetId: bigint, content: string) {
    this.assertDistinct(senderId, targetId);
    const normalizedContent = content.trim();
    if (!normalizedContent || normalizedContent.length > 1000) throw new BadRequestException('私聊内容需为 1-1000 个字符');
    await this.assertNotBlockedEitherWay(senderId, targetId);
    await this.usersService.findOne(targetId);
    const message = await this.store.createDirectMessage({ sender_id: senderId, recipient_id: targetId, content: normalizedContent, expires_at: new Date(Date.now() + RETENTION_MS) });
    const sender = await this.buildProfile(targetId, senderId, false);
    const payload = { t: 'dm_message', message: { id: message.id.toString(), senderId: senderId.toString(), recipientId: targetId.toString(), content: message.content, createdAt: message.created_at, sender } };
    await this.sendToUser(targetId, payload);
    return payload.message;
  }

  async listConversation(userId: bigint, otherUserId: bigint, limit: number, before?: Date) {
    await this.assertNotBlockedEitherWay(userId, otherUserId);
    const messages = await this.store.listDirectMessages(userId, otherUserId, limit, before);
    const other = await this.buildProfile(userId, otherUserId, false);
    return { other, messages: messages.reverse().map((message) => this.serializeDirectMessage(message)), unreadCount: await this.store.countUnreadDirectMessages(userId) };
  }

  async listConversations(userId: bigint) {
    const latest = await this.store.listConversations(userId);
    return Promise.all(latest.map(async (message) => {
      const otherId = message.sender_id === userId ? message.recipient_id : message.sender_id;
      return { other: await this.buildProfile(userId, otherId, false), latestMessage: this.serializeDirectMessage(message) };
    }));
  }

  async markConversationRead(userId: bigint, otherUserId: bigint) {
    const affected = await this.store.markDirectMessagesRead(userId, otherUserId);
    await this.sendToUser(otherUserId, { t: 'dm_read', readerId: userId.toString() });
    return { affected };
  }

  async getNotificationSummary(userId: bigint, limit: number, before?: Date) {
    const [notifications, unreadCount, unreadMessages] = await Promise.all([
      this.store.listNotifications(userId, limit, before),
      this.store.countUnreadNotifications(userId),
      this.store.countUnreadDirectMessages(userId),
    ]);
    return { notifications: notifications.map((notification) => this.serializeNotification(notification)), unreadCount, unreadMessages };
  }

  async markNotificationRead(userId: bigint, notificationId: bigint) {
    const notification = await this.store.markNotificationRead(userId, notificationId);
    if (!notification) throw new NotFoundException('通知不存在');
    return this.serializeNotification(notification);
  }

  async markAllNotificationsRead(userId: bigint) { return { affected: await this.store.markAllNotificationsRead(userId) }; }

  async getTravelDestinations(userId: bigint) {
    const unlocks = await this.store.listUnlocks(userId);
    const unlocked = new Set(unlocks.map((unlock) => unlock.destination_id));
    return TRAVEL_DESTINATIONS.map((destination) => ({ ...destination, unlocked: Boolean(('initial' in destination && destination.initial) || unlocked.has(destination.id)) }));
  }

  async discoverDestination(userId: bigint, destinationId: string) {
    const destination = this.destination(destinationId);
	if (!('initial' in destination && destination.initial)) await this.assertAtTravelDestination(userId, destination);
    await this.store.createUnlock(userId, destination.id);
    return { ...destination, unlocked: true };
  }

  async travelTo(userId: bigint, destinationId: string) {
    const destination = this.destination(destinationId);
    const unlocked = ('initial' in destination && destination.initial) || (await this.store.listUnlocks(userId)).some((unlock) => unlock.destination_id === destinationId);
    if (!unlocked) throw new ForbiddenException('该地点尚未解锁');
    return { ...destination, unlocked: true };
  }

  async canSeeChat(senderId: string, recipientId: string): Promise<boolean> {
    if (!/^\d+$/.test(senderId) || !/^\d+$/.test(recipientId)) return false;
    return !(await this.isBlockedEitherWay(BigInt(senderId), BigInt(recipientId)));
  }

  async notifyPresenceChanged(userId: string, online: boolean): Promise<void> {
    if (!/^\d+$/.test(userId)) return;
    const friends = await this.store.listFriendships(BigInt(userId));
    for (const friendship of friends) {
      const otherId = friendship.user_low_id === BigInt(userId) ? friendship.user_high_id : friendship.user_low_id;
      await this.sendToUser(otherId, { t: 'friend_presence_changed', userId, online });
    }
  }

  @Cron(CronExpression.EVERY_HOUR)
  async cleanupExpiredData(): Promise<void> {
    await this.store.cleanupExpired(new Date());
  }

  async purgeTestUsers(userIds: bigint[]): Promise<void> {
    await this.store.purgeUsers(userIds);
  }

  private async buildProfile(viewerId: bigint, targetId: bigint, includePrivate: boolean) {
    const [user, profile, socketId] = await Promise.all([
      this.usersService.findOne(targetId),
      this.ensureProfile(targetId),
      this.sessions.getSocketIdByUserId(targetId.toString()),
    ]);
    const tags = this.profileTags(profile);
    const session = socketId ? await this.sessions.getSession(socketId) : null;
    const testPresence = getTestLabPresence(targetId);
    const isFriend = viewerId === targetId ? false : await this.areFriends(viewerId, targetId);
    const blockedEitherWay = viewerId === targetId ? false : await this.isBlockedEitherWay(viewerId, targetId);
    return {
      id: targetId.toString(),
      username: user.username,
      nickname: user.nickname,
      avatarUrl: user.avatar_url || '',
      skinId: profile.skin_id || '',
      online: Boolean(socketId) || Boolean(testPresence?.online),
      currentArea: testPresence?.mapId || session?.currentMap || profile.current_map,
      bio: String(profile.bio || '').slice(0, 160),
      interests: this.validInterests(tags.interests),
      privacy: includePrivate ? this.privacy(profile) : undefined,
      isFriend: includePrivate || viewerId === targetId ? false : isFriend,
      blocked: includePrivate ? false : blockedEitherWay,
      room_visitable: viewerId === targetId || (!blockedEitherWay && this.roomVisitPolicy(profile) === 'public') || (!blockedEitherWay && this.roomVisitPolicy(profile) === 'friends' && isFriend),
    };
  }

  private async ensureProfile(userId: bigint): Promise<ProfileRecord> {
    const existing = await this.profiles.findByUserId(userId);
    if (existing) return existing;
    await this.accountProfileService.ensureProfile(userId);
    const profile = await this.profiles.findByUserId(userId);
    if (!profile) throw new NotFoundException('用户档案不存在');
    return profile;
  }

  private profileTags(profile: ProfileRecord): Record<string, any> {
    return profile.tags && typeof profile.tags === 'object' ? { ...profile.tags } : {};
  }

  private privacy(profile: ProfileRecord): Record<string, boolean> {
    const tags = this.profileTags(profile);
    const values = tags[SOCIAL_SETTINGS_KEY];
    return { ...DEFAULT_PRIVACY, ...(values && typeof values === 'object' ? values : {}) };
  }

  private roomVisitPolicy(profile: ProfileRecord): 'friends' | 'public' | 'closed' {
    const value = this.profileTags(profile)[SOCIAL_SETTINGS_KEY];
    if (value && typeof value === 'object' && !Array.isArray(value)) {
      const policy = String((value as Record<string, unknown>).room_visit_policy || 'friends');
      if (ROOM_VISIT_POLICIES.has(policy)) return policy as 'friends' | 'public' | 'closed';
    }
    return 'friends';
  }

  private async canVisitRoomWithProfile(viewerId: bigint, ownerId: bigint, profile: ProfileRecord): Promise<boolean> {
    if (viewerId === ownerId) return true;
    if (await this.isBlockedEitherWay(viewerId, ownerId)) return false;
    const policy = this.roomVisitPolicy(profile);
    if (policy === 'public') return true;
    return policy === 'friends' && await this.areFriends(viewerId, ownerId);
  }

  private validInterests(value: unknown): string[] {
    return Array.isArray(value) ? value.map(String).filter((item) => INTEREST_IDS.has(item)).slice(0, 3) : [];
  }

  private async assertNearbyAllowed(sourceId: bigint, targetId: bigint, purpose: 'profile' | 'private' | 'friend') {
    const targetSocketId = await this.sessions.getSocketIdByUserId(targetId.toString());
    const sourceSocketId = await this.sessions.getSocketIdByUserId(sourceId.toString());
    const targetTestPresence = getTestLabPresence(targetId);
    const sourceTestPresence = getTestLabPresence(sourceId);
    const [target, source, profile] = await Promise.all([
      targetSocketId ? this.sessions.getSession(targetSocketId) : Promise.resolve(null),
      sourceSocketId ? this.sessions.getSession(sourceSocketId) : Promise.resolve(null),
      this.ensureProfile(targetId),
    ]);
    const targetPosition = targetTestPresence?.online
      ? { mapId: targetTestPresence.mapId, x: targetTestPresence.x, y: targetTestPresence.y }
      : target ? { mapId: target.currentMap, x: Number(target.position?.x || 0), y: Number(target.position?.y || 0) } : null;
    const sourcePosition = sourceTestPresence?.online
      ? { mapId: sourceTestPresence.mapId, x: sourceTestPresence.x, y: sourceTestPresence.y }
      : source ? { mapId: source.currentMap, x: Number(source.position?.x || 0), y: Number(source.position?.y || 0) } : null;
    if (!targetPosition || !sourcePosition) throw new ForbiddenException('陌生玩家需要在线且在附近才能互动');
    if (targetPosition.mapId !== sourcePosition.mapId) throw new ForbiddenException('陌生玩家仅可在同一地图互动');
    const distance = Math.hypot(targetPosition.x - sourcePosition.x, targetPosition.y - sourcePosition.y);
    if (distance > NEARBY_DISTANCE) throw new ForbiddenException('请靠近该玩家后再互动');
    const privacy = this.privacy(profile);
    const key = purpose === 'profile' ? 'allow_nearby_profile' : purpose === 'private' ? 'allow_nearby_private' : 'allow_nearby_friend_requests';
    if (!privacy[key]) throw new ForbiddenException('对方已关闭此类附近互动');
  }

  private async areFriends(userA: bigint, userB: bigint): Promise<boolean> {
    const [low, high] = this.sortIds(userA, userB);
    return Boolean(await this.store.findFriendship(low, high));
  }

  private async isBlockedEitherWay(userA: bigint, userB: bigint): Promise<boolean> {
    const [aBlocksB, bBlocksA] = await Promise.all([this.store.isBlocked(userA, userB), this.store.isBlocked(userB, userA)]);
    return aBlocksB || bBlocksA;
  }

  private async assertNotBlockedEitherWay(userA: bigint, userB: bigint) {
    if (await this.isBlockedEitherWay(userA, userB)) throw new ForbiddenException('该互动当前不可用');
  }

  private async createNotification(userId: bigint, category: string, title: string, content: string, actionMetadata: Record<string, unknown>) {
    const notification = await this.store.createNotification({ user_id: userId, category, title, content, action_metadata: actionMetadata, expires_at: new Date(Date.now() + RETENTION_MS) });
    await this.sendToUser(userId, { t: 'notification_created', notification: this.serializeNotification(notification) });
    return notification;
  }

  private async sendToUser(userId: bigint, payload: Record<string, unknown>) {
    const socketId = await this.sessions.getSocketIdByUserId(userId.toString());
    if (socketId && this.realtimeGateway) this.realtimeGateway.sendToPlayer(socketId, payload);
  }

  private serializeDirectMessage(message: any) {
    return { id: message.id.toString(), senderId: message.sender_id.toString(), recipientId: message.recipient_id.toString(), content: message.content, createdAt: message.created_at, readAt: message.read_at || null };
  }
  private serializeNotification(notification: any) {
    return { id: notification.id.toString(), category: notification.category, title: notification.title, content: notification.content, actionMetadata: notification.action_metadata || {}, createdAt: notification.created_at, readAt: notification.read_at || null, expiresAt: notification.expires_at };
  }
  private destination(destinationId: string) {
    const destination = TRAVEL_DESTINATIONS.find((item) => item.id === destinationId);
    if (!destination) throw new NotFoundException('未知地点');
    return destination;
  }
  private async assertAtTravelDestination(userId: bigint, destination: (typeof TRAVEL_DESTINATIONS)[number]) {
    const socketId = await this.sessions.getSocketIdByUserId(userId.toString());
    const session = socketId ? await this.sessions.getSession(socketId) : null;
    if (!session || session.currentMap !== destination.mapId) throw new ForbiddenException('需要先在该地点附近探索');
    const origin = TRAVEL_MAP_ORIGINS[destination.mapId] || { x: 0, y: 0 };
    const targetX = destination.x - origin.x;
    const targetY = destination.y - origin.y;
    const distance = Math.hypot(Number(session.position?.x || 0) - targetX, Number(session.position?.y || 0) - targetY);
    if (distance > DISCOVERY_DISTANCE) throw new ForbiddenException('需要靠近地点后才能解锁');
  }
  private assertDistinct(userA: bigint, userB: bigint) { if (userA === userB) throw new BadRequestException('不能对自己执行此操作'); }
  private sortIds(userA: bigint, userB: bigint): [bigint, bigint] { return userA < userB ? [userA, userB] : [userB, userA]; }
}
