import { BadRequestException, ForbiddenException, Injectable, NotFoundException, } from '@nestjs/common'; import { and, eq, or } from 'drizzle-orm'; import { DrizzleService } from '../../database/drizzle.service'; import { chat, match, message, profile, report, user } from '../../database/schema'; import { NotificationsService } from '../../notifications/notifications.service'; import { CreateChatDto } from './dto/create-chat.dto'; import { CloseChatDto, CloseChatType } from './dto/close-chat.dto'; import { SendMessageDto } from './dto/send-message.dto'; @Injectable() export class ChatService { constructor( private readonly drizzleService: DrizzleService, private readonly notificationsService: NotificationsService, ) {} async createChat(userId: string, dto: CreateChatDto) { await this.assertProfileOwnership(userId, dto.profileId); const [foundMatch] = await this.drizzleService.db .select() .from(match) .where( and( eq(match.id, dto.matchId), or(eq(match.profile1Id, dto.profileId), eq(match.profile2Id, dto.profileId)), ), ) .limit(1); if (!foundMatch) throw new NotFoundException('Match not found'); const existingChat = await this.drizzleService.db .select() .from(chat) .where( or( and(eq(chat.profile1Id, foundMatch.profile1Id), eq(chat.profile2Id, foundMatch.profile2Id)), and(eq(chat.profile1Id, foundMatch.profile2Id), eq(chat.profile2Id, foundMatch.profile1Id)), ), ) .limit(1); if (existingChat.length > 0) return existingChat[0]; const [currentProfile] = await this.drizzleService.db .select({ activeChatId: profile.activeChatId }) .from(profile) .where(eq(profile.id, dto.profileId)) .limit(1); if (currentProfile?.activeChatId) { throw new BadRequestException( 'Profile already has an active chat. Close it before opening a new one.', ); } const [newChat] = await this.drizzleService.db .insert(chat) .values({ profile1Id: foundMatch.profile1Id, profile2Id: foundMatch.profile2Id, status: 'active', } as any) .returning(); await this.drizzleService.db .update(profile) .set({ activeChatId: newChat.id } as any) .where(or(eq(profile.id, foundMatch.profile1Id), eq(profile.id, foundMatch.profile2Id))); return newChat; } async closeChat(userId: string, profileId: string, chatId: string, dto: CloseChatDto) { await this.assertProfileOwnership(userId, profileId); const [foundChat] = await this.drizzleService.db .select() .from(chat) .where(eq(chat.id, chatId)) .limit(1); if (!foundChat) throw new NotFoundException('Chat not found'); if (foundChat.profile1Id !== profileId && foundChat.profile2Id !== profileId) { throw new ForbiddenException('Not a chat participant'); } const otherProfileId = foundChat.profile1Id === profileId ? foundChat.profile2Id : foundChat.profile1Id; let messageText: string; if (dto.type === CloseChatType.Report) { if (!dto.reportReason) throw new BadRequestException('reportReason is required for type=report'); messageText = dto.reportDescription ? `Жалоба: ${dto.reportReason}. ${dto.reportDescription}` : `Жалоба: ${dto.reportReason}`; await this.drizzleService.db .insert(report) .values({ sourceProfileId: profileId, entityId: otherProfileId, entityType: 'profile', description: messageText, } as any); } else { messageText = dto.cancelMessage?.trim() || 'Диалог завершён'; } await this.drizzleService.db .insert(message) .values({ chatId, profileId, text: messageText, mediaUrl: null, mediaType: null, } as any); await this.drizzleService.db .update(chat) .set({ status: 'closed' } as any) .where(eq(chat.id, chatId)); await this.drizzleService.db .update(profile) .set({ activeChatId: null } as any) .where(or(eq(profile.id, foundChat.profile1Id), eq(profile.id, foundChat.profile2Id))); return { message: 'Chat closed' }; } async getChatsForProfile(userId: string, profileId: string) { await this.assertProfileOwnership(userId, profileId); return this.drizzleService.db .select() .from(chat) .where(or(eq(chat.profile1Id, profileId), eq(chat.profile2Id, profileId))); } async getChatMessages(userId: string, profileId: string, chatId: string, page = 1, limit = 50) { await this.assertProfileOwnership(userId, profileId); const [foundChat] = await this.drizzleService.db .select() .from(chat) .where(eq(chat.id, chatId)) .limit(1); if (!foundChat) throw new NotFoundException('Chat not found'); if (foundChat.profile1Id !== profileId && foundChat.profile2Id !== profileId) { throw new ForbiddenException('Not a chat participant'); } const offset = (page - 1) * limit; return this.drizzleService.db .select() .from(message) .where(eq(message.chatId, chatId)) .orderBy(message.createdAt) .limit(limit) .offset(offset); } async sendMessage(userId: string, profileId: string, chatId: string, dto: SendMessageDto) { await this.assertProfileOwnership(userId, profileId); const [foundChat] = await this.drizzleService.db .select() .from(chat) .where(and(eq(chat.id, chatId), eq(chat.status, 'active'))) .limit(1); if (!foundChat) throw new NotFoundException('Active chat not found'); if (foundChat.profile1Id !== profileId && foundChat.profile2Id !== profileId) { throw new ForbiddenException('Not a chat participant'); } if (!dto.text && !dto.mediaUrl) { throw new BadRequestException('Message must have text or media'); } const [newMessage] = await this.drizzleService.db .insert(message) .values({ chatId, profileId, text: dto.text || null, mediaUrl: dto.mediaUrl || null, mediaType: dto.mediaType || null, } as any) .returning(); const recipientProfileId = foundChat.profile1Id === profileId ? foundChat.profile2Id : foundChat.profile1Id; const [recipientProfile] = await this.drizzleService.db .select({ userId: profile.userId }) .from(profile) .where(eq(profile.id, recipientProfileId)) .limit(1); if (recipientProfile) { const [recipientUser] = await this.drizzleService.db .select({ fcmToken: user.fcmToken }) .from(user) .where(eq(user.id, recipientProfile.userId)) .limit(1); if (recipientUser?.fcmToken) { await this.notificationsService.sendPushNotification( recipientUser.fcmToken, 'New message', dto.text?.substring(0, 100) || 'Media message', { chatId, messageId: newMessage.id, type: 'message' }, ); } } return newMessage; } private async assertProfileOwnership(userId: string, profileId: string) { const [found] = await this.drizzleService.db .select({ userId: profile.userId }) .from(profile) .where(eq(profile.id, profileId)) .limit(1); if (!found) throw new NotFoundException('Profile not found'); if (found.userId !== userId) throw new ForbiddenException('Profile does not belong to you'); } }