import {
  ForbiddenException,
  Injectable,
  NotFoundException,
} from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Conversation } from '@/entities/conversation.entity';
import { ConversationMember } from '@/entities/conversation-member.entity';
import { Message } from '@/entities/message.entity';
import { MessageReaction } from '@/entities/message-reaction.entity';
import { Group } from '@/entities/group.entity';
import { Repository } from 'typeorm';
import { ChatGateway } from './chat.gateway';
import { PushService } from '@/push/push.service';
import { GroupMember } from '@/entities/group-member.entity';

@Injectable()
export class ChatService {
  constructor(
    @InjectRepository(Conversation)
    private readonly conversationRepo: Repository<Conversation>,
    @InjectRepository(ConversationMember)
    private readonly memberRepo: Repository<ConversationMember>,
    @InjectRepository(Message)
    private readonly messageRepo: Repository<Message>,
    @InjectRepository(MessageReaction)
    private readonly reactionRepo: Repository<MessageReaction>,
    @InjectRepository(GroupMember)
    private readonly groupMemberRepo: Repository<GroupMember>,
    private readonly gateway: ChatGateway,
    private readonly pushService: PushService,
  ) {}

  async ensureForGroup(groupId: string, userIds: string[] = []) {
    let conversation = await this.conversationRepo.findOne({
      where: { group: { id: groupId } },
    });
    if (!conversation) {
      conversation = await this.conversationRepo.save(
        this.conversationRepo.create({ group: { id: groupId } as Group }),
      );
    }
    for (const userId of userIds) {
      await this.addMember(groupId, userId);
    }
    return conversation;
  }

  async addMember(groupId: string, userId: string) {
    const conversation = await this.ensureForGroup(groupId);
    const exists = await this.memberRepo.findOne({
      where: { conversation: { id: conversation.id }, user: { id: userId } },
    });
    if (!exists) {
      await this.memberRepo.save(
        this.memberRepo.create({
          conversation: { id: conversation.id },
          user: { id: userId },
          lastReadAt: new Date(),
        }),
      );
    }
    this.gateway.joinUser(userId, conversation.id);
  }

  async removeMember(groupId: string, userId: string) {
    const conversation = await this.conversationRepo.findOne({
      where: { group: { id: groupId } },
    });
    if (!conversation) return;
    await this.memberRepo.delete({
      conversation: { id: conversation.id },
      user: { id: userId },
    });
    this.gateway.removeUser(userId, conversation.id);
  }

  private async membership(groupId: string, userId: string) {
    let member = await this.memberRepo.findOne({
      where: {
        conversation: { group: { id: groupId } },
        user: { id: userId },
      },
      relations: ['conversation', 'conversation.group'],
    });
    if (!member) {
      const groupMembers = await this.groupMemberRepo.find({
        where: { group: { id: groupId } },
        relations: ['user'],
      });
      if (groupMembers.some((item) => item.user.id === userId)) {
        await this.ensureForGroup(
          groupId,
          groupMembers.map((item) => item.user.id),
        );
        member = await this.memberRepo.findOne({
          where: {
            conversation: { group: { id: groupId } },
            user: { id: userId },
          },
          relations: ['conversation', 'conversation.group'],
        });
      }
    }
    if (!member) throw new ForbiddenException('Bạn không thuộc hội thoại này');
    return member;
  }

  async getConversation(groupId: string, userId: string) {
    const member = await this.membership(groupId, userId);
    const conversation = await this.conversationRepo.findOne({
      where: { id: member.conversation.id },
      relations: ['group', 'members', 'members.user'],
    });
    return {
      ...conversation,
      unreadCount: await this.messageRepo
        .createQueryBuilder('m')
        .where('m.conversationId = :id', { id: conversation!.id })
        .andWhere('m.createdAt > :lastReadAt', {
          lastReadAt: member.lastReadAt || new Date(0),
        })
        .andWhere('m.senderId != :userId', { userId })
        .getCount(),
    };
  }

  async messages(groupId: string, userId: string, page = 1, limit = 30) {
    const member = await this.membership(groupId, userId);
    const [data, total] = await this.messageRepo.findAndCount({
      where: { conversation: { id: member.conversation.id } },
      relations: ['sender', 'reactions', 'reactions.user', 'pinnedBy'],
      select: {
        sender: { id: true, name: true, avatar: true },
        reactions: { id: true, emoji: true, user: { id: true, name: true } },
      },
      order: { createdAt: 'DESC' },
      skip: (page - 1) * limit,
      take: limit,
    });
    const readers = await this.memberRepo.find({
      where: { conversation: { id: member.conversation.id } },
      relations: ['user'],
      select: { user: { id: true, name: true }, lastReadAt: true },
    });
    return { data: data.reverse(), total, page, limit, readers };
  }

  async send(groupId: string, userId: string, content: string) {
    const member = await this.membership(groupId, userId);
    const message = await this.messageRepo.save(
      this.messageRepo.create({
        conversation: { id: member.conversation.id },
        sender: { id: userId },
        content: content.trim(),
      }),
    );
    const result = await this.messageRepo.findOne({
      where: { id: message.id },
      relations: ['sender', 'reactions'],
      select: { sender: { id: true, name: true, avatar: true } },
    });
    const members = await this.memberRepo.find({
      where: { conversation: { id: member.conversation.id } },
      relations: ['user'],
    });
    this.gateway.sendToConversation(member.conversation.id, 'chat:message', result);
    members
      .filter((item) => item.user.id !== userId)
      .forEach((item) => {
        this.gateway.sendToUser(item.user.id, 'chat:notification', {
          groupId,
          conversationId: member.conversation.id,
          message: result,
        });
        void this.pushService.sendToUser(item.user.id, {
          title: result?.sender?.name || 'Tin nhắn mới',
          body: result?.content || '',
          type: 'chat_message',
          groupId,
          conversationId: member.conversation.id,
          messageId: result?.id || '',
        });
      });
    return result;
  }

  async markRead(groupId: string, userId: string) {
    const member = await this.membership(groupId, userId);
    member.lastReadAt = new Date();
    await this.memberRepo.save(member);
    this.gateway.sendToConversation(member.conversation.id, 'chat:read', {
      userId,
      readAt: member.lastReadAt,
    });
    return { success: true, readAt: member.lastReadAt };
  }

  private async getMessage(messageId: string, userId: string) {
    const message = await this.messageRepo.findOne({
      where: { id: messageId },
      relations: ['conversation', 'conversation.group'],
    });
    if (!message) throw new NotFoundException('Không tìm thấy tin nhắn');
    await this.membership(message.conversation.group.id, userId);
    return message;
  }

  async react(messageId: string, userId: string, emoji: string) {
    const message = await this.getMessage(messageId, userId);
    const existing = await this.reactionRepo.findOne({
      where: { message: { id: messageId }, user: { id: userId }, emoji },
    });
    if (existing) await this.reactionRepo.remove(existing);
    else
      await this.reactionRepo.save(
        this.reactionRepo.create({
          message: { id: messageId },
          user: { id: userId },
          emoji,
        }),
      );
    const reactions = await this.reactionRepo.find({
      where: { message: { id: messageId } },
      relations: ['user'],
      select: { id: true, emoji: true, user: { id: true, name: true } },
    });
    this.gateway.sendToConversation(message.conversation.id, 'chat:reaction', {
      messageId,
      reactions,
    });
    return reactions;
  }

  async pin(messageId: string, userId: string) {
    const message = await this.getMessage(messageId, userId);
    message.isPinned = !message.isPinned;
    message.pinnedAt = message.isPinned ? new Date() : null;
    message.pinnedBy = message.isPinned ? ({ id: userId } as any) : null;
    await this.messageRepo.save(message);
    const payload = {
      messageId,
      isPinned: message.isPinned,
      pinnedAt: message.pinnedAt,
      pinnedBy: message.pinnedBy,
    };
    this.gateway.sendToConversation(message.conversation.id, 'chat:pin', payload);
    return payload;
  }
}
