import { BadRequestException, ForbiddenException, Injectable, NotFoundException } from '@nestjs/common';
import { PrismaService } from '../infra/prisma.service';
import { MinioService } from '../infra/minio.service';
import { resolveVerificationIcon } from './verification.constants';
import { FamilyService } from './family.service';
import { NotificationsService } from './notifications.service';

interface PollPayload {
  question: string;
  options: string[];
  allowsMultiple?: boolean;
  isAnonymous?: boolean;
}

interface ReadContextMember {
  userId: string;
  lastReadAt: Date | null;
}

@Injectable()
export class ChatService {
  constructor(
    private readonly prisma: PrismaService,
    private readonly family: FamilyService,
    private readonly notifications: NotificationsService,
    private readonly minio: MinioService
  ) {}

  async listRooms(userId: string, groupId: string) {
    await this.family.assertFamilyMember(groupId, userId);
    await this.syncFamilyChats(groupId);

    await this.prisma.chatRoom.updateMany({
      where: { groupId, type: 'DIRECT', isE2E: true },
      data: { isE2E: false }
    });

    let generalRoom = await this.prisma.chatRoom.findFirst({ where: { groupId, type: 'GENERAL' } });
    if (!generalRoom) {
      const members = await this.prisma.familyMember.findMany({ where: { groupId } });
      generalRoom = await this.prisma.chatRoom.create({
        data: {
          groupId,
          type: 'GENERAL',
          name: 'Общий чат',
          members: { create: members.filter((member) => member.role !== 'bot').map((member) => ({ userId: member.userId })) }
        }
      });
    }

    const rooms = await this.prisma.chatRoom.findMany({
      where: {
        groupId,
        members: { some: { userId } }
      },
      include: {
        members: { include: { user: true } },
        messages: {
          orderBy: { createdAt: 'desc' },
          take: 1,
          include: {
            sender: true,
            poll: { include: { options: { include: { votes: true } } } }
          }
        }
      },
      orderBy: [{ type: 'asc' }, { updatedAt: 'desc' }]
    });

    const dedupedRooms = rooms.filter((room) => {
      if (room.type !== 'GROUP' || room.members.length !== 2) return true;
      const memberIds = room.members.map((member) => member.userId).sort().join(':');
      return !rooms.some(
        (candidate) =>
          candidate.id !== room.id &&
          (candidate.type === 'DIRECT' || candidate.type === 'BOT') &&
          candidate.members.map((member) => member.userId).sort().join(':') === memberIds
      );
    });

    const familyMembers = await this.prisma.familyMember.findMany({ where: { groupId } });
    const familyRoleByUserId = new Map(familyMembers.map((member) => [member.userId, member.role]));

    const formatted = await Promise.all(
      dedupedRooms.map(async (room) => {
        let lastMessage = room.messages[0]
          ? await this.toMessage(room.messages[0], userId, await this.getRoomReadContext(room.id))
          : undefined;

        if (room.type === 'BOT' && room.botUsername) {
          const botPreview = await this.getBotRoomPreview(userId, room.botUsername);
          if (botPreview) {
            lastMessage = botPreview;
          }
        }

        return this.formatRoom(
          room,
          familyRoleByUserId,
          lastMessage,
          userId
        );
      })
    );

    formatted.sort((a, b) => {
      const pinDiff = Number(Boolean(b.isPinned)) - Number(Boolean(a.isPinned));
      if (pinDiff !== 0) return pinDiff;
      if (a.isPinned && b.isPinned) {
        return (b.pinnedAt ?? '').localeCompare(a.pinnedAt ?? '');
      }
      const rank = (type: string) => (type === 'GENERAL' ? 0 : 1);
      const rankDiff = rank(a.type) - rank(b.type);
      if (rankDiff !== 0) return rankDiff;
      const aTime = a.lastMessage?.createdAt ?? a.updatedAt;
      const bTime = b.lastMessage?.createdAt ?? b.updatedAt;
      return bTime.localeCompare(aTime);
    });

    return { rooms: formatted };
  }

  async syncFamilyChats(groupId: string) {
    const members = await this.prisma.familyMember.findMany({
      where: { groupId },
      include: { user: { include: { linkedBot: { select: { username: true } } } } }
    });

    const humans = members.filter((member) => !member.user.linkedBotId);
    const legacyRooms = await this.prisma.chatRoom.findMany({
      where: { groupId, type: 'GROUP' },
      include: { members: true }
    });

    for (const room of legacyRooms) {
      if (room.members.length !== 2) continue;
      const memberIds = room.members.map((member) => member.userId);
      const memberA = members.find((member) => member.userId === memberIds[0]);
      const memberB = members.find((member) => member.userId === memberIds[1]);
      const aIsBot = Boolean(memberA?.user.linkedBotId);
      const bIsBot = Boolean(memberB?.user.linkedBotId);

      if (aIsBot || bIsBot) {
        const humanId = aIsBot ? memberIds[1]! : memberIds[0]!;
        const botId = aIsBot ? memberIds[0]! : memberIds[1]!;
        const botMember = members.find((member) => member.userId === botId);
        await this.prisma.chatRoom.update({
          where: { id: room.id },
          data: {
            type: 'BOT',
            peerUserId: botId,
            botUsername: botMember?.user.linkedBot?.username ?? undefined,
            name: botMember?.user.displayName ?? room.name,
            isE2E: false
          }
        });
      } else {
        await this.prisma.chatRoom.update({
          where: { id: room.id },
          data: {
            type: 'DIRECT',
            isE2E: false,
            peerUserId: memberIds[1],
            name: memberB?.user.displayName ?? room.name
          }
        });
      }
    }

    for (let i = 0; i < humans.length; i += 1) {
      for (let j = i + 1; j < humans.length; j += 1) {
        const left = humans[i]!;
        const right = humans[j]!;
        await this.ensureDirectRoom(groupId, left.userId, right.userId, right.user.displayName);
      }
    }

    // BOT-чаты создаются только для тех людей, которым явно выдали доступ к боту.
    // Не создаём комнаты "каждый человек x каждый бот", иначе бот становится видимым всей семье.
  }

  async ensureBotRoomForMember(groupId: string, requesterId: string, botUserId: string) {
    const group = await this.family.assertFamilyMember(groupId, requesterId);
    const botMember = group.members.find((member) => member.userId === botUserId);
    if (!botMember?.user?.linkedBotId || !botMember.user.linkedBot) {
      throw new BadRequestException('Бот не найден в этой семье');
    }
    return this.ensureBotRoom(
      groupId,
      requesterId,
      botUserId,
      botMember.user.linkedBot.username,
      botMember.user.displayName
    );
  }

  async createRoom(userId: string, groupId: string, name: string, memberUserIds: string[]) {
    const group = await this.family.assertFamilyMember(groupId, userId);
    const trimmedName = name.trim();
    if (!trimmedName) {
      throw new BadRequestException('Укажите название группового чата');
    }

    const uniqueMembers = Array.from(new Set([userId, ...memberUserIds]));
    if (uniqueMembers.length === 2) {
      throw new BadRequestException('Личный чат создаётся автоматически при добавлении участника в семью');
    }
    for (const memberId of uniqueMembers) {
      if (!group.members.some((member) => member.userId === memberId)) {
        throw new BadRequestException('Все участники чата должны состоять в семье');
      }
    }

    const room = await this.prisma.chatRoom.create({
      data: {
        groupId,
        type: 'GROUP',
        name: trimmedName,
        createdById: userId,
        members: {
          create: uniqueMembers.map((memberId) => ({ userId: memberId }))
        }
      },
      include: {
        members: { include: { user: true } },
        messages: true
      }
    });

    const familyMembers = await this.prisma.familyMember.findMany({ where: { groupId } });
    const familyRoleByUserId = new Map(familyMembers.map((member) => [member.userId, member.role]));

    return this.formatRoom(room, familyRoleByUserId, undefined, userId);
  }

  async createE2ERoom(userId: string, groupId: string, peerUserId: string) {
    if (userId === peerUserId) {
      throw new BadRequestException('Нельзя создать секретный чат с самим собой');
    }

    const group = await this.family.assertFamilyMember(groupId, userId);
    if (!group.members.some((member) => member.userId === peerUserId)) {
      throw new BadRequestException('Пользователь не состоит в семье');
    }

    const peerUser = await this.prisma.user.findUnique({
      where: { id: peerUserId },
      select: { displayName: true, linkedBotId: true, isSystemAccount: true }
    });
    if (!peerUser || peerUser.linkedBotId || peerUser.isSystemAccount) {
      throw new BadRequestException('Секретный E2E чат доступен только с участниками семьи');
    }

    const existing = await this.findPairRoom(groupId, userId, peerUserId, 'E2E');
    if (existing) {
      return this.getRoomResponse(existing.id, userId);
    }

    const room = await this.prisma.chatRoom.create({
      data: {
        groupId,
        type: 'E2E',
        name: peerUser.displayName,
        peerUserId,
        isE2E: true,
        members: {
          create: [{ userId }, { userId: peerUserId }]
        }
      },
      include: {
        members: { include: { user: true } },
        messages: true
      }
    });

    const familyMembers = await this.prisma.familyMember.findMany({ where: { groupId } });
    const familyRoleByUserId = new Map(familyMembers.map((member) => [member.userId, member.role]));
    return this.formatRoom(room, familyRoleByUserId, undefined, userId);
  }

  async deleteRoom(userId: string, roomId: string) {
    const room = await this.getRoomForMember(roomId, userId);
    if (room.type === 'GENERAL') {
      throw new BadRequestException('Нельзя удалить общий чат семьи');
    }

    if (room.type === 'GROUP') {
      const isFamilyOwner = room.group.ownerId === userId;
      const isChatCreator = room.createdById === userId;
      if (!isFamilyOwner && !isChatCreator) {
        throw new ForbiddenException('Удалить групповой чат может только создатель чата или семьи');
      }
    }

    const messages = await this.prisma.chatMessage.findMany({
      where: { roomId },
      select: { storageKey: true }
    });
    const storageKeys = messages
      .map((message) => message.storageKey)
      .filter((key): key is string => Boolean(key));
    if (room.avatarStorageKey) {
      storageKeys.push(room.avatarStorageKey);
    }

    await this.prisma.chatRoom.delete({ where: { id: roomId } });

    if (storageKeys.length) {
      await this.minio.deleteObjects(storageKeys).catch(() => undefined);
    }

    return { count: 1 };
  }

  async addRoomMember(userId: string, roomId: string, memberUserId: string) {
    const room = await this.getRoomForMember(roomId, userId);
    if (room.type === 'GENERAL') {
      throw new BadRequestException('В общий чат нельзя добавлять участников вручную');
    }
    if (room.type === 'DIRECT' || room.type === 'E2E') {
      throw new BadRequestException('Нельзя изменять состав личного чата или чата с ботом');
    }
    if (room.members.some((member) => member.userId === memberUserId)) {
      throw new BadRequestException('Пользователь уже состоит в чате');
    }
    if (!room.group.members.some((member) => member.userId === memberUserId)) {
      throw new BadRequestException('Пользователь должен состоять в семье');
    }
    if (room.type === 'BOT') {
      const targetUser = await this.prisma.user.findUnique({
        where: { id: memberUserId },
        select: { linkedBotId: true, isSystemAccount: true }
      });
      if (!targetUser || targetUser.linkedBotId || targetUser.isSystemAccount) {
        throw new BadRequestException('В чат с ботом можно добавить только обычного участника семьи');
      }
    }

    await this.prisma.chatRoomMember.create({
      data: { roomId, userId: memberUserId }
    });

    return this.getRoomResponse(roomId, userId);
  }

  async removeRoomMember(userId: string, roomId: string, memberUserId: string) {
    const room = await this.getRoomForMember(roomId, userId);
    if (room.type === 'GENERAL') {
      throw new BadRequestException('Нельзя удалить участника из общего чата');
    }
    if (room.type === 'DIRECT' || room.type === 'E2E') {
      throw new BadRequestException('Нельзя изменять состав личного чата или чата с ботом');
    }

    const isFamilyOwner = room.group.ownerId === userId;
    const isChatCreator = room.createdById === userId;
    if (room.type !== 'BOT' && !isFamilyOwner && !isChatCreator) {
      throw new ForbiddenException('Недостаточно прав для удаления участника из чата');
    }
    if (room.type === 'BOT' && memberUserId === room.peerUserId) {
      throw new BadRequestException('Нельзя удалить бота из его чата');
    }

    if (memberUserId === room.createdById && !isFamilyOwner) {
      throw new BadRequestException('Создателя чата может удалить только создатель семьи');
    }

    if (memberUserId === userId && room.members.length <= (room.type === 'BOT' ? 2 : 1)) {
      throw new BadRequestException('В чате должен остаться хотя бы один участник');
    }

    const membership = room.members.find((member) => member.userId === memberUserId);
    if (!membership) {
      throw new NotFoundException('Участник не найден в этом чате');
    }

    await this.prisma.chatRoomMember.delete({
      where: { roomId_userId: { roomId, userId: memberUserId } }
    });

    return this.getRoomResponse(roomId, userId);
  }

  async updateRoomSettings(userId: string, roomId: string, name?: string, notificationsMuted?: boolean, pinned?: boolean) {
    const room = await this.getRoomForMember(roomId, userId);
    if (name !== undefined) {
      const trimmedName = name.trim();
      if (!trimmedName) {
        throw new BadRequestException('Укажите название чата');
      }
      if (room.type === 'GENERAL') {
        throw new BadRequestException('Нельзя переименовать общий чат');
      }
      if (room.type === 'DIRECT' || room.type === 'BOT' || room.type === 'E2E') {
        throw new BadRequestException('Нельзя переименовать личный чат или чат с ботом');
      }
      await this.prisma.chatRoom.update({ where: { id: roomId }, data: { name: trimmedName } });
    }
    if (notificationsMuted !== undefined) {
      await this.prisma.chatRoomMember.update({
        where: { roomId_userId: { roomId, userId } },
        data: { notificationsMuted }
      });
    }
    if (pinned !== undefined) {
      await this.prisma.chatRoomMember.update({
        where: { roomId_userId: { roomId, userId } },
        data: { pinnedAt: pinned ? new Date() : null }
      });
    }
    return this.getRoomResponse(roomId, userId);
  }

  async listMessages(userId: string, roomId: string, beforeMessageId?: string, limit = 50) {
    const room = await this.getRoomForMember(roomId, userId);
    if (room.type === 'BOT') {
      throw new BadRequestException('Сообщения бота загружаются через Bot API');
    }
    const take = Math.min(Math.max(limit, 1), 100);
    const readContext = await this.getRoomReadContext(roomId);

    let cursorDate: Date | undefined;
    if (beforeMessageId) {
      const cursor = await this.prisma.chatMessage.findFirst({ where: { id: beforeMessageId, roomId } });
      if (cursor) cursorDate = cursor.createdAt;
    }

    const messages = await this.prisma.chatMessage.findMany({
      where: {
        roomId,
        ...(cursorDate ? { createdAt: { lt: cursorDate } } : {})
      },
      include: {
        sender: true,
        poll: { include: { options: { include: { votes: true } } } }
      },
      orderBy: { createdAt: 'desc' },
      take
    });

    const ordered = messages.reverse();
    return {
      messages: await Promise.all(ordered.map((message) => this.toMessage(message, userId, readContext)))
    };
  }

  async sendMessage(
    userId: string,
    roomId: string,
    type: string,
    content?: string,
    replyToId?: string,
    storageKey?: string,
    mimeType?: string,
    metadataJson?: string,
    poll?: PollPayload,
    isEncrypted?: boolean
  ) {
    const room = await this.getRoomForMember(roomId, userId);
    if (room.type === 'BOT') {
      throw new BadRequestException('Сообщения боту отправляйте через Bot API');
    }
    const requiresEncryption = room.type === 'E2E' || (room.type === 'DIRECT' && room.isE2E);
    if (requiresEncryption && !isEncrypted) {
      throw new BadRequestException('Сообщения секретного чата должны быть зашифрованы на клиенте');
    }
    if (room.type === 'E2E' && type === 'POLL') {
      throw new BadRequestException('Опросы недоступны в секретном E2E чате');
    }
    const allowedTypes = new Set(['TEXT', 'IMAGE', 'AUDIO', 'VOICE', 'FILE', 'EMOJI', 'POLL']);
    if (!allowedTypes.has(type)) {
      throw new BadRequestException('Неподдерживаемый тип сообщения');
    }

    if (type === 'POLL') {
      if (!poll?.question?.trim() || !poll.options?.length) {
        throw new BadRequestException('Укажите вопрос и варианты опроса');
      }
      if (poll.options.length < 2) {
        throw new BadRequestException('В опросе должно быть минимум 2 варианта');
      }
    } else if (type !== 'IMAGE' && type !== 'AUDIO' && type !== 'VOICE' && type !== 'FILE') {
      if (!content?.trim()) {
        throw new BadRequestException('Сообщение не может быть пустым');
      }
    }

    if ((type === 'IMAGE' || type === 'AUDIO' || type === 'VOICE' || type === 'FILE') && !storageKey) {
      throw new BadRequestException('Не указан файл для отправки');
    }

    if (storageKey && !storageKey.startsWith(`chat/${roomId}/`)) {
      throw new BadRequestException('Некорректный ключ медиафайла');
    }

    const metadata = metadataJson ? JSON.parse(metadataJson) : undefined;

    const message = await this.prisma.$transaction(async (tx) => {
      const created = await tx.chatMessage.create({
        data: {
          roomId,
          senderId: userId,
          type,
          content: content?.trim() || null,
          isEncrypted: Boolean(isEncrypted),
          replyToId: replyToId || null,
          storageKey: storageKey || null,
          mimeType: mimeType || null,
          metadata: metadata ?? undefined
        },
        include: { sender: true }
      });

      if (type === 'POLL' && poll) {
        await tx.chatPoll.create({
          data: {
            messageId: created.id,
            question: poll.question.trim(),
            allowsMultiple: poll.allowsMultiple ?? false,
            isAnonymous: poll.isAnonymous ?? false,
            options: {
              create: poll.options.map((text) => ({ text: text.trim() })).filter((item) => item.text)
            }
          }
        });
      }

      await tx.chatRoom.update({ where: { id: roomId }, data: { updatedAt: new Date() } });
      return created;
    });

    const fullMessage = await this.prisma.chatMessage.findUnique({
      where: { id: message.id },
      include: {
        sender: true,
        poll: { include: { options: { include: { votes: true } } } }
      }
    });

    const readContext = await this.getRoomReadContext(roomId);
    const response = await this.toMessage(fullMessage!, userId, readContext);
    await this.notifyRoomMembers(room, userId, response);
    return response;
  }

  async publishRoomAvatarChangedMessage(roomId: string, userId: string) {
    const room = await this.prisma.chatRoom.findUnique({
      where: { id: roomId },
      include: { members: true }
    });
    if (!room || (room.type !== 'GROUP' && room.type !== 'GENERAL')) {
      return null;
    }

    const user = await this.prisma.user.findUnique({ where: { id: userId } });
    const userName = user?.displayName?.trim() || 'Участник';
    const content = `${userName} изменил(а) фото чата`;

    const message = await this.prisma.chatMessage.create({
      data: {
        roomId,
        senderId: userId,
        type: 'SYSTEM',
        content,
        metadata: { isSystem: true, systemKind: 'avatar_changed' }
      },
      include: { sender: true }
    });

    await this.prisma.chatRoom.update({ where: { id: roomId }, data: { updatedAt: new Date() } });

    const readContext = await this.getRoomReadContext(roomId);
    const response = await this.toMessage(message, userId, readContext);
    await this.publishRoomEvent(
      { id: room.id, name: room.name, members: room.members },
      'chat_message',
      response
    );
    return response;
  }

  async editMessage(userId: string, messageId: string, content: string) {
    const message = await this.prisma.chatMessage.findUnique({
      where: { id: messageId },
      include: { room: { include: { members: true } } }
    });
    if (!message || message.deletedAt) {
      throw new NotFoundException('Сообщение не найдено');
    }
    if (message.senderId !== userId) {
      throw new ForbiddenException('Можно редактировать только свои сообщения');
    }
    if (message.isEncrypted) {
      throw new BadRequestException('Зашифрованные сообщения нельзя редактировать');
    }
    if (!['TEXT', 'EMOJI'].includes(message.type)) {
      throw new BadRequestException('Этот тип сообщения нельзя редактировать');
    }

    const trimmed = content.trim();
    if (!trimmed) {
      throw new BadRequestException('Сообщение не может быть пустым');
    }

    await this.prisma.chatMessage.update({
      where: { id: messageId },
      data: { content: trimmed, editedAt: new Date() }
    });

    const response = await this.loadMessageResponse(messageId, userId);
    await this.publishRoomEvent(message.room, 'chat_message_updated', response);
    return response;
  }

  async deleteMessage(userId: string, messageId: string) {
    const message = await this.prisma.chatMessage.findUnique({
      where: { id: messageId },
      include: { room: { include: { members: true } } }
    });
    if (!message || message.deletedAt) {
      throw new NotFoundException('Сообщение не найдено');
    }
    if (message.senderId !== userId) {
      throw new ForbiddenException('Можно удалять только свои сообщения');
    }

    await this.prisma.chatMessage.update({
      where: { id: messageId },
      data: { deletedAt: new Date(), content: null, storageKey: null, mimeType: null, metadata: undefined }
    });

    const response = await this.loadMessageResponse(messageId, userId);
    await this.publishRoomEvent(message.room, 'chat_message_deleted', response);
    return response;
  }

  async setMessagePinned(userId: string, messageId: string, pinned: boolean) {
    const message = await this.prisma.chatMessage.findUnique({
      where: { id: messageId },
      include: { room: { include: { members: true } } }
    });
    if (!message || message.deletedAt) {
      throw new NotFoundException('Сообщение не найдено');
    }
    if (message.room.type === 'BOT') {
      throw new BadRequestException('Сообщения бота закрепляются через Bot API');
    }
    if (!message.room.members.some((member) => member.userId === userId)) {
      throw new ForbiddenException('Вы не состоите в этом чате');
    }

    await this.prisma.chatMessage.update({
      where: { id: messageId },
      data: pinned
        ? { isPinned: true, pinnedAt: new Date(), pinnedById: userId }
        : { isPinned: false, pinnedAt: null, pinnedById: null }
    });

    const response = await this.loadMessageResponse(messageId, userId);
    await this.publishRoomEvent(message.room, 'chat_message_updated', response);
    return response;
  }

  async toggleMessageReaction(userId: string, messageId: string, emoji: string) {
    const normalizedEmoji = emoji.trim();
    if (!normalizedEmoji || normalizedEmoji.length > 16) {
      throw new BadRequestException('Некорректная реакция');
    }

    const message = await this.prisma.chatMessage.findUnique({
      where: { id: messageId },
      include: { room: { include: { members: true } } }
    });
    if (!message || message.deletedAt) {
      throw new NotFoundException('Сообщение не найдено');
    }
    if (!message.room.members.some((member) => member.userId === userId)) {
      throw new ForbiddenException('Вы не состоите в этом чате');
    }

    const meta = (message.metadata as Record<string, unknown> | null) ?? {};
    const reactions = { ...((meta.reactions as Record<string, string[]>) ?? {}) };
    const current = reactions[normalizedEmoji] ?? [];
    if (current.includes(userId)) {
      const next = current.filter((id) => id !== userId);
      if (next.length) {
        reactions[normalizedEmoji] = next;
      } else {
        delete reactions[normalizedEmoji];
      }
    } else {
      reactions[normalizedEmoji] = [...current, userId];
    }

    await this.prisma.chatMessage.update({
      where: { id: messageId },
      data: { metadata: { ...meta, reactions } }
    });

    const response = await this.loadMessageResponse(messageId, userId);
    await this.publishRoomEvent(message.room, 'chat_message_updated', response);
    return response;
  }

  async forwardMessages(userId: string, targetRoomId: string, messageIds: string[]) {
    if (!messageIds.length) {
      throw new BadRequestException('Укажите сообщения для пересылки');
    }
    const targetRoom = await this.getRoomForMember(targetRoomId, userId);
    if (targetRoom.type === 'BOT') {
      throw new BadRequestException('Нельзя пересылать сообщения в чат с ботом');
    }

    const uniqueIds = [...new Set(messageIds)];
    const sourceMessages = await this.prisma.chatMessage.findMany({
      where: { id: { in: uniqueIds }, deletedAt: null },
      include: {
        sender: true,
        room: { include: { members: true } }
      },
      orderBy: { createdAt: 'asc' }
    });

    if (sourceMessages.length !== uniqueIds.length) {
      throw new NotFoundException('Одно или несколько сообщений не найдены');
    }

    for (const message of sourceMessages) {
      if (!message.room.members.some((member) => member.userId === userId)) {
        throw new ForbiddenException('Нет доступа к одному из сообщений');
      }
      if (message.isEncrypted) {
        throw new BadRequestException('Зашифрованные сообщения нельзя пересылать');
      }
    }

    const forwarded: Awaited<ReturnType<ChatService['loadMessageResponse']>>[] = [];
    for (const source of sourceMessages) {
      let forwardType = source.type;
      let forwardContent = source.content ?? undefined;
      let forwardStorageKey = source.storageKey ?? undefined;
      let forwardMimeType = source.mimeType ?? undefined;

      if (!['TEXT', 'EMOJI'].includes(source.type)) {
        forwardType = 'TEXT';
        forwardContent = this.buildForwardPreview(source.type, source.content);
        forwardStorageKey = undefined;
        forwardMimeType = undefined;
      }

      const metadata = {
        forwardedFrom: {
          messageId: source.id,
          roomId: source.roomId,
          senderId: source.senderId,
          senderName: source.sender.displayName,
          type: source.type,
          content: source.content ?? undefined
        }
      };
      const created = await this.sendMessage(
        userId,
        targetRoomId,
        forwardType,
        forwardContent,
        undefined,
        forwardStorageKey,
        forwardMimeType,
        JSON.stringify(metadata),
        undefined,
        false
      );
      forwarded.push(created);
    }

    return { messages: forwarded };
  }

  async reportTyping(userId: string, roomId: string) {
    const room = await this.getRoomForMember(roomId, userId);
    const user = await this.prisma.user.findUnique({ where: { id: userId } });
    const userName = user?.displayName?.trim() || user?.phone || 'Участник';

    for (const member of room.members) {
      if (member.userId === userId) continue;
      await this.notifications.publishRealtime(member.userId, 'chat_typing', room.name, '', {
        roomId: room.id,
        userId,
        userName
      });
    }

    return { success: true };
  }

  private buildForwardPreview(type: string, content?: string | null) {
    switch (type) {
      case 'IMAGE':
        return '📷 Пересланное фото';
      case 'VOICE':
        return '🎤 Пересланное голосовое сообщение';
      case 'AUDIO':
        return '🎵 Пересланное аудио';
      case 'FILE':
        return content ? `📄 Пересланный файл: ${content}` : '📄 Пересланный файл';
      case 'POLL':
        return '📊 Пересланный опрос';
      default:
        return '↪️ Пересланное сообщение';
    }
  }

  async markRoomRead(userId: string, roomId: string, lastMessageId?: string) {
    const room = await this.getRoomForMember(roomId, userId);
    let readAt = new Date();

    if (lastMessageId) {
      const message = await this.prisma.chatMessage.findFirst({ where: { id: lastMessageId, roomId } });
      if (message) {
        readAt = message.createdAt;
      }
    }

    await this.prisma.chatRoomMember.update({
      where: { roomId_userId: { roomId, userId } },
      data: { lastReadAt: readAt, lastReadMessageId: lastMessageId ?? null }
    });

    for (const member of room.members) {
      if (member.userId === userId) continue;
      await this.notifications.publishRealtime(member.userId, 'chat_read_receipt', room.name, '', {
        roomId,
        userId,
        lastReadAt: readAt.toISOString(),
        lastReadMessageId: lastMessageId
      });
    }

    return { count: 1 };
  }

  async votePoll(userId: string, messageId: string, optionIds: string[]) {
    const message = await this.prisma.chatMessage.findUnique({
      where: { id: messageId },
      include: {
        poll: { include: { options: true } },
        room: { include: { members: true, group: true } }
      }
    });
    if (!message?.poll) {
      throw new NotFoundException('Опрос не найден');
    }
    await this.getRoomForMember(message.roomId, userId);

    const validOptionIds = new Set(message.poll.options.map((option) => option.id));
    for (const optionId of optionIds) {
      if (!validOptionIds.has(optionId)) {
        throw new BadRequestException('Некорректный вариант опроса');
      }
    }
    if (!message.poll.allowsMultiple && optionIds.length > 1) {
      throw new BadRequestException('Можно выбрать только один вариант');
    }

    await this.prisma.$transaction(async (tx) => {
      await tx.chatPollVote.deleteMany({
        where: {
          userId,
          option: { pollId: message.poll!.id }
        }
      });
      for (const optionId of optionIds) {
        await tx.chatPollVote.create({ data: { optionId, userId } });
      }
    });

    const updated = await this.prisma.chatMessage.findUnique({
      where: { id: messageId },
      include: {
        sender: true,
        poll: { include: { options: { include: { votes: true } } } }
      }
    });
    return this.toMessage(updated!, userId, await this.getRoomReadContext(updated!.roomId));
  }

  async setRoomNotificationsMuted(userId: string, roomId: string, muted: boolean) {
    await this.getRoomForMember(roomId, userId);
    await this.prisma.chatRoomMember.update({
      where: { roomId_userId: { roomId, userId } },
      data: { notificationsMuted: muted }
    });
    return { count: 1 };
  }

  async assertRoomMember(roomId: string, userId: string) {
    return this.getRoomForMember(roomId, userId);
  }

  private async getRoomForMember(roomId: string, userId: string) {
    const room = await this.prisma.chatRoom.findUnique({
      where: { id: roomId },
      include: {
        members: true,
        group: { include: { members: true } }
      }
    });
    if (!room) {
      throw new NotFoundException('Чат не найден');
    }
    if (!room.members.some((member) => member.userId === userId)) {
      throw new ForbiddenException('Вы не состоите в этом чате');
    }
    return room;
  }

  private formatRoom(
    room: {
      id: string;
      groupId: string;
      type: string;
      name: string;
      peerUserId?: string | null;
      botUsername?: string | null;
      isE2E?: boolean;
      hasAvatar: boolean;
      updatedAt: Date;
      createdById: string | null;
      members: Array<{
        id: string;
        userId: string;
        notificationsMuted: boolean;
        pinnedAt?: Date | null;
        user: { displayName: string; avatarStorageKey: string | null; isVerified: boolean; verificationIcon: string | null; linkedBotId?: string | null };
      }>;
    },
    familyRoleByUserId: Map<string, string>,
    lastMessage?: Awaited<ReturnType<ChatService['toMessage']>>,
    viewerId?: string
  ) {
    const viewerMembership = viewerId ? room.members.find((member) => member.userId === viewerId) : undefined;
    const peerMember =
      room.type === 'BOT'
        ? room.members.find((member) => Boolean(member.user.linkedBotId))
        : viewerId && (room.type === 'DIRECT' || room.type === 'E2E')
          ? room.members.find((member) => member.userId !== viewerId)
          : undefined;

    return {
      id: room.id,
      groupId: room.groupId,
      type: room.type,
      name: peerMember?.user.displayName ?? room.name,
      peerUserId: peerMember?.userId ?? (room.peerUserId && room.peerUserId !== viewerId ? room.peerUserId : undefined),
      botUsername: room.botUsername ?? undefined,
      isE2E: room.isE2E ?? false,
      hasAvatar: room.hasAvatar,
      createdById: room.createdById ?? undefined,
      updatedAt: room.updatedAt.toISOString(),
      isPinned: Boolean(viewerMembership?.pinnedAt),
      pinnedAt: viewerMembership?.pinnedAt?.toISOString(),
      members: room.members.map((member) => ({
        id: member.id,
        userId: member.userId,
        displayName: member.user.displayName,
        hasAvatar: Boolean(member.user.avatarStorageKey),
        isVerified: member.user.isVerified,
        verificationIcon: member.user.isVerified ? resolveVerificationIcon(member.user.verificationIcon) : undefined,
        notificationsMuted: member.notificationsMuted,
        isPinned: Boolean(member.pinnedAt),
        pinnedAt: member.pinnedAt?.toISOString(),
        familyRole: familyRoleByUserId.get(member.userId) ?? 'member',
        isChatCreator: room.createdById === member.userId
      })),
      lastMessage
    };
  }

  private async getRoomResponse(roomId: string, userId: string) {
    const room = await this.getRoomForMember(roomId, userId);
    const full = await this.prisma.chatRoom.findUnique({
      where: { id: roomId },
      include: { members: { include: { user: true } } }
    });
    const familyMembers = await this.prisma.familyMember.findMany({ where: { groupId: full!.groupId } });
    const familyRoleByUserId = new Map(familyMembers.map((member) => [member.userId, member.role]));
    return this.formatRoom(full!, familyRoleByUserId, undefined, userId);
  }

  private async getRoomReadContext(roomId: string): Promise<ReadContextMember[]> {
    const members = await this.prisma.chatRoomMember.findMany({ where: { roomId } });
    return members.map((member) => ({ userId: member.userId, lastReadAt: member.lastReadAt }));
  }

  private async loadMessageResponse(messageId: string, viewerId: string) {
    const message = await this.prisma.chatMessage.findUnique({
      where: { id: messageId },
      include: {
        sender: true,
        poll: { include: { options: { include: { votes: true } } } }
      }
    });
    if (!message) {
      throw new NotFoundException('Сообщение не найдено');
    }
    const readContext = await this.getRoomReadContext(message.roomId);
    return this.toMessage(message, viewerId, readContext);
  }

  private async publishRoomEvent(
    room: { id: string; name: string; members: Array<{ userId: string }> },
    eventType: string,
    message: Awaited<ReturnType<ChatService['toMessage']>>
  ) {
    for (const member of room.members) {
      await this.notifications.publishRealtime(member.userId, eventType, room.name, '', {
        roomId: room.id,
        message
      });
    }
  }

  private async notifyRoomMembers(
    room: { id: string; name: string; members: Array<{ userId: string; notificationsMuted: boolean }> },
    senderId: string,
    message: Awaited<ReturnType<ChatService['toMessage']>>
  ) {
    const sender = await this.prisma.user.findUnique({ where: { id: senderId } });
    const preview =
      message.isEncrypted
        ? '🔒 Зашифрованное сообщение'
        : message.type === 'SYSTEM'
          ? (message.content ?? 'Системное сообщение')
        : message.type === 'TEXT' || message.type === 'EMOJI'
        ? (message.content ?? '')
        : message.type === 'POLL'
          ? '📊 Опрос'
          : message.type === 'VOICE'
            ? '🎤 Голосовое'
            : message.type === 'AUDIO'
              ? '🎵 Аудио'
              : message.type === 'FILE'
                ? '📄 Файл'
                : message.type === 'IMAGE'
                  ? '📷 Фото'
                  : '📎 Медиа';

    for (const member of room.members) {
      if (member.userId === senderId) continue;
      await this.notifications.publishRealtime(member.userId, 'chat_message', room.name, preview.slice(0, 120), {
        roomId: room.id,
        message
      });
    }

    for (const member of room.members) {
      if (member.userId === senderId || member.notificationsMuted) continue;
      await this.notifications.create(
        member.userId,
        'chat_message',
        room.name,
        `${sender?.displayName ?? 'Участник'}: ${preview.slice(0, 120)}`,
        { roomId: room.id, messageId: message.id },
        { skipPublish: true }
      );
    }
  }

  private async toMessage(
    message: {
      id: string;
      roomId: string;
      senderId: string;
      type: string;
      content: string | null;
      isEncrypted?: boolean;
      replyToId: string | null;
      storageKey: string | null;
      mimeType: string | null;
      metadata: unknown;
      isPinned?: boolean;
      pinnedAt?: Date | null;
      pinnedById?: string | null;
      createdAt: Date;
      editedAt?: Date | null;
      deletedAt?: Date | null;
      sender: { displayName: string; avatarStorageKey: string | null; isVerified: boolean; verificationIcon: string | null };
      poll?: {
        id: string;
        question: string;
        allowsMultiple: boolean;
        isAnonymous: boolean;
        closedAt: Date | null;
        options: Array<{ id: string; text: string; votes: Array<{ userId: string }> }>;
      } | null;
    },
    viewerId: string,
    readContext?: ReadContextMember[]
  ) {
    const isDeleted = Boolean(message.deletedAt);
    let readByAll = false;
    if (readContext && message.senderId === viewerId && !isDeleted) {
      const others = readContext.filter((member) => member.userId !== message.senderId);
      readByAll =
        others.length > 0 && others.every((member) => member.lastReadAt && member.lastReadAt >= message.createdAt);
    }

    return {
      id: message.id,
      roomId: message.roomId,
      senderId: message.senderId,
      senderName: message.sender.displayName,
      senderHasAvatar: Boolean(message.sender.avatarStorageKey),
      senderIsVerified: message.sender.isVerified,
      senderVerificationIcon: message.sender.isVerified ? resolveVerificationIcon(message.sender.verificationIcon) : undefined,
      type: message.type,
      content: isDeleted ? undefined : message.content ?? undefined,
      isEncrypted: Boolean(message.isEncrypted),
      replyToId: message.replyToId ?? undefined,
      storageKey: isDeleted ? undefined : message.storageKey ?? undefined,
      mimeType: isDeleted ? undefined : message.mimeType ?? undefined,
      metadataJson: isDeleted ? undefined : message.metadata ? JSON.stringify(message.metadata) : undefined,
      createdAt: message.createdAt.toISOString(),
      editedAt: message.editedAt?.toISOString(),
      isDeleted,
      readByAll: message.senderId === viewerId ? readByAll : undefined,
      reactions: isDeleted ? [] : this.buildReactionResponses(message.metadata, viewerId),
      isPinned: !isDeleted && Boolean(message.isPinned),
      pinnedAt: !isDeleted ? message.pinnedAt?.toISOString() : undefined,
      pinnedById: !isDeleted ? message.pinnedById ?? undefined : undefined,
      poll: !isDeleted && message.poll
        ? {
            id: message.poll.id,
            question: message.poll.question,
            allowsMultiple: message.poll.allowsMultiple,
            isAnonymous: message.poll.isAnonymous,
            closedAt: message.poll.closedAt?.toISOString(),
            options: message.poll.options.map((option) => ({
              id: option.id,
              text: option.text,
              voteCount: option.votes.length,
              votedByMe: option.votes.some((vote) => vote.userId === viewerId)
            }))
          }
        : undefined
    };
  }

  private buildReactionResponses(metadata: unknown, viewerId: string) {
    const meta = metadata as { reactions?: Record<string, string[]> } | null;
    const reactions = meta?.reactions;
    if (!reactions) return [];
    return Object.entries(reactions)
      .filter(([, userIds]) => userIds.length > 0)
      .map(([emoji, userIds]) => ({
        emoji,
        count: userIds.length,
        reactedByMe: userIds.includes(viewerId)
      }));
  }

  private async findPairRoom(groupId: string, userA: string, userB: string, type: 'DIRECT' | 'BOT' | 'E2E') {
    const rooms = await this.prisma.chatRoom.findMany({
      where: { groupId, type },
      include: { members: true }
    });
    return rooms.find((room) => {
      const ids = new Set(room.members.map((member) => member.userId));
      return ids.size === 2 && ids.has(userA) && ids.has(userB);
    });
  }

  private async ensureDirectRoom(groupId: string, userA: string, userB: string, peerName: string) {
    const existing = await this.findPairRoom(groupId, userA, userB, 'DIRECT');
    if (existing) return existing;

    return this.prisma.chatRoom.create({
      data: {
        groupId,
        type: 'DIRECT',
        name: peerName,
        peerUserId: userB,
        isE2E: false,
        members: {
          create: [{ userId: userA }, { userId: userB }]
        }
      }
    });
  }

  private async ensureBotRoom(
    groupId: string,
    humanUserId: string,
    botUserId: string,
    botUsername: string,
    botDisplayName: string
  ) {
    const existing = await this.findPairRoom(groupId, humanUserId, botUserId, 'BOT');
    if (existing) {
      if (!existing.botUsername) {
        await this.prisma.chatRoom.update({
          where: { id: existing.id },
          data: { botUsername, peerUserId: botUserId, name: botDisplayName }
        });
      }
      return existing;
    }

    return this.prisma.chatRoom.create({
      data: {
        groupId,
        type: 'BOT',
        name: botDisplayName,
        peerUserId: botUserId,
        botUsername,
        createdById: humanUserId,
        isE2E: false,
        members: {
          create: [{ userId: humanUserId }, { userId: botUserId }]
        }
      }
    });
  }

  private async getBotRoomPreview(userId: string, botUsername: string) {
    const bot = await this.prisma.bot.findFirst({
      where: {
        OR: [{ username: botUsername }, { username: `${botUsername.replace(/_bot$/i, '')}_bot` }]
      },
      select: { id: true, name: true }
    });
    if (!bot) return undefined;

    const chat = await this.prisma.botChat.findUnique({
      where: { botId_recipientUserId: { botId: bot.id, recipientUserId: userId } }
    });
    if (!chat) return undefined;

    const [lastInbound, lastOutbound] = await Promise.all([
      this.prisma.botInboundMessage.findFirst({
        where: { botChatId: chat.id },
        orderBy: { createdAt: 'desc' }
      }),
      this.prisma.botMessage.findFirst({
        where: { botChatId: chat.id },
        orderBy: { createdAt: 'desc' }
      })
    ]);

    const candidates = [lastInbound, lastOutbound].filter(Boolean) as Array<{ createdAt: Date; text: string | null }>;
    if (!candidates.length) return undefined;

    const latest = candidates.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime())[0]!;
    return {
      id: `bot-preview-${chat.id}`,
      roomId: '',
      senderId: userId,
      senderName: bot.name,
      senderHasAvatar: false,
      senderIsVerified: true,
      senderVerificationIcon: resolveVerificationIcon('bot'),
      type: 'TEXT',
      content: latest.text ?? '',
      isEncrypted: false,
      replyToId: undefined,
      storageKey: undefined,
      mimeType: undefined,
      metadataJson: undefined,
      createdAt: latest.createdAt.toISOString(),
      editedAt: undefined,
      isDeleted: false,
      readByAll: undefined,
      reactions: [],
      isPinned: false,
      pinnedAt: undefined,
      pinnedById: undefined,
      poll: undefined
    };
  }
}
