import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { FilterQuery, Model, Types } from 'mongoose';
import { ChatBlock, ChatBlockDocument } from './schemas/chat-block.schema';
import { Conversation, ConversationDocument } from './schemas/conversation.schema';
import { Message, MessageDocument } from './schemas/message.schema';

@Injectable()
export class ChatRepository {
  constructor(
    @InjectModel(Conversation.name) private readonly conversationModel: Model<ConversationDocument>,
    @InjectModel(Message.name) private readonly messageModel: Model<MessageDocument>,
    @InjectModel(ChatBlock.name) private readonly chatBlockModel: Model<ChatBlockDocument>,
  ) {}

  async findConversationById(id: string): Promise<ConversationDocument | null> {
    return this.conversationModel.findById(id).exec();
  }

  async findDirectConversation(userAId: string, userBId: string): Promise<ConversationDocument | null> {
    return this.conversationModel
      .findOne({
        isGroup: false,
        participantIds: {
          $all: [new Types.ObjectId(userAId), new Types.ObjectId(userBId)],
          $size: 2,
        },
      })
      .exec();
  }

  async createConversation(payload: {
    participantIds: string[];
    isGroup: boolean;
    title?: string;
    createdBy: string;
  }): Promise<ConversationDocument> {
    const participantIdStrings = payload.participantIds.map((id) => id.toString());
    const participantIds = participantIdStrings.map((id) => new Types.ObjectId(id));
    const unreadCountByUser: Record<string, number> = {};
    participantIdStrings.forEach((id) => {
      unreadCountByUser[id] = 0;
    });

    return this.conversationModel.create({
      participantIds,
      isGroup: payload.isGroup,
      title: payload.title ?? '',
      createdBy: new Types.ObjectId(payload.createdBy),
      unreadCountByUser,
      lastMessageText: '',
    });
  }

  private visibleConversationFilterForUser(userId: string): FilterQuery<ConversationDocument> {
    const userObjectId = new Types.ObjectId(userId);
    return {
      participantIds: userObjectId,
      $expr: {
        $let: {
          vars: {
            hiddenEntry: {
              $first: {
                $filter: {
                  input: { $ifNull: ['$hiddenFor', []] },
                  as: 'hidden',
                  cond: { $eq: ['$$hidden.userId', userObjectId] },
                },
              },
            },
          },
          in: {
            $or: [
              { $eq: ['$$hiddenEntry', null] },
              {
                $gt: [
                  { $ifNull: ['$lastMessageAt', new Date(0)] },
                  '$$hiddenEntry.hiddenAt',
                ],
              },
            ],
          },
        },
      },
    };
  }

  async findConversationsForUser(
    userId: string,
    skip: number,
    limit: number,
    sort: Record<string, 1 | -1> = { lastMessageAt: -1, updatedAt: -1 },
  ): Promise<ConversationDocument[]> {
    return this.conversationModel
      .find(this.visibleConversationFilterForUser(userId))
      .populate({ path: 'participantIds', select: 'name username stageName avatar isVerified isDisabled' })
      .sort(sort)
      .skip(skip)
      .limit(limit)
      .exec();
  }

  async countConversationsForUser(userId: string): Promise<number> {
    return this.conversationModel.countDocuments(this.visibleConversationFilterForUser(userId)).exec();
  }

  async hideConversationForUser(
    conversationId: string,
    userId: string,
    hiddenAt: Date,
  ): Promise<ConversationDocument | null> {
    const conversationObjectId = new Types.ObjectId(conversationId);
    const userObjectId = new Types.ObjectId(userId);

    await this.conversationModel
      .updateOne(
        { _id: conversationObjectId, participantIds: userObjectId },
        { $pull: { hiddenFor: { userId: userObjectId } } },
      )
      .exec();

    return this.conversationModel
      .findOneAndUpdate(
        { _id: conversationObjectId, participantIds: userObjectId },
        {
          $push: {
            hiddenFor: {
              userId: userObjectId,
              hiddenAt,
            },
          },
        },
        { new: true },
      )
      .exec();
  }

  async createMessage(payload: {
    conversationId: string;
    senderId: string;
    content?: string;
    messageType: 'text' | 'image' | 'video' | 'audio';
    mediaUrl?: string;
    replyToMessageId?: string | null;
  }): Promise<MessageDocument> {
    return this.messageModel.create({
      conversationId: new Types.ObjectId(payload.conversationId),
      senderId: new Types.ObjectId(payload.senderId),
      content: payload.content ?? '',
      messageType: payload.messageType,
      mediaUrl: payload.mediaUrl ?? '',
      replyToMessageId: payload.replyToMessageId ? new Types.ObjectId(payload.replyToMessageId) : null,
      seenBy: [new Types.ObjectId(payload.senderId)],
      isUnsent: false,
    });
  }

  async findMessages(
    conversationId: string,
    userId: string,
    skip: number,
    limit: number,
    sort: Record<string, 1 | -1> = { createdAt: -1 },
  ): Promise<MessageDocument[]> {
    return this.messageModel
      .find({
        conversationId: new Types.ObjectId(conversationId),
        deletedForUserIds: { $ne: new Types.ObjectId(userId) },
      })
      .populate({ path: 'senderId', select: 'name username stageName avatar isVerified' })
      .populate({
        path: 'replyToMessageId',
        select: 'content messageType mediaUrl senderId isUnsent',
        populate: { path: 'senderId', select: 'name username stageName avatar isVerified' },
      })
      .sort(sort)
      .skip(skip)
      .limit(limit)
      .exec();
  }

  async countMessages(conversationId: string, userId: string): Promise<number> {
    return this.messageModel
      .countDocuments({
        conversationId: new Types.ObjectId(conversationId),
        deletedForUserIds: { $ne: new Types.ObjectId(userId) },
      })
      .exec();
  }

  async findMessageById(messageId: string): Promise<MessageDocument | null> {
    return this.messageModel.findById(messageId).exec();
  }

  async markMessageSeen(messageId: string, userId: string): Promise<void> {
    await this.messageModel
      .findByIdAndUpdate(messageId, { $addToSet: { seenBy: new Types.ObjectId(userId) } }, { new: false })
      .exec();
  }

  async markMessageDelivered(messageId: string, userId: string, deliveredAt: Date): Promise<void> {
    await this.messageModel
      .updateOne(
        {
          _id: new Types.ObjectId(messageId),
          'deliveredBy.userId': { $ne: new Types.ObjectId(userId) },
        },
        {
          $push: {
            deliveredBy: {
              userId: new Types.ObjectId(userId),
              deliveredAt,
            },
          },
        },
      )
      .exec();
  }

  async setMessageReaction(
    messageId: string,
    userId: string,
    reactionType: string,
  ): Promise<MessageDocument | null> {
    return this.messageModel
      .findByIdAndUpdate(
        messageId,
        { $set: { [`reactionsByUser.${userId.toString()}`]: reactionType } },
        { new: true },
      )
      .populate({ path: 'senderId', select: 'name username stageName avatar isVerified' })
      .exec();
  }

  async deleteMessageForUser(messageId: string, userId: string): Promise<void> {
    await this.messageModel
      .findByIdAndUpdate(messageId, { $addToSet: { deletedForUserIds: new Types.ObjectId(userId) } })
      .exec();
  }

  async updateConversationAfterNewMessage(
    conversationId: string,
    messageId: string,
    senderId: string,
    messageText: string,
  ): Promise<ConversationDocument | null> {
    const conversation = await this.conversationModel.findById(conversationId).exec();
    if (!conversation) {
      return null;
    }

    const senderKey = senderId.toString();
    const incrementUnreadCounts: Record<string, number> = {};
    for (const participantId of conversation.participantIds) {
      const id = participantId.toString();
      if (id !== senderKey) {
        incrementUnreadCounts[`unreadCountByUser.${id}`] = 1;
      }
    }

    const update: Record<string, unknown> = {
      $set: {
        lastMessageId: new Types.ObjectId(messageId),
        lastMessageText: messageText.slice(0, 4000),
        lastMessageAt: new Date(),
        [`unreadCountByUser.${senderKey}`]: 0,
      },
    };
    if (Object.keys(incrementUnreadCounts).length > 0) {
      update.$inc = incrementUnreadCounts;
    }

    return this.conversationModel.findByIdAndUpdate(conversationId, update, { new: true }).exec();
  }

  async clearConversationUnreadForUser(conversationId: string, userId: string): Promise<void> {
    await this.conversationModel
      .updateOne(
        { _id: new Types.ObjectId(conversationId) },
        {
          $set: {
            [`unreadCountByUser.${userId.toString()}`]: 0,
          },
        },
      )
      .exec();
  }

  async unsendMessage(messageId: string, senderId: string): Promise<MessageDocument | null> {
    return this.messageModel
      .findOneAndUpdate(
        { _id: new Types.ObjectId(messageId), senderId: new Types.ObjectId(senderId) },
        {
          isUnsent: true,
          content: '',
          mediaUrl: '',
          messageType: 'text',
        },
        { new: true },
      )
      .exec();
  }

  async findManyMessages(filter: FilterQuery<MessageDocument>): Promise<MessageDocument[]> {
    return this.messageModel.find(filter).exec();
  }

  async createBlock(blockerId: string, blockedId: string): Promise<void> {
    await this.chatBlockModel
      .updateOne(
        { blockerId: new Types.ObjectId(blockerId), blockedId: new Types.ObjectId(blockedId) },
        {
          $setOnInsert: {
            blockerId: new Types.ObjectId(blockerId),
            blockedId: new Types.ObjectId(blockedId),
          },
        },
        { upsert: true },
      )
      .exec();
  }

  async removeBlock(blockerId: string, blockedId: string): Promise<void> {
    await this.chatBlockModel
      .deleteOne({ blockerId: new Types.ObjectId(blockerId), blockedId: new Types.ObjectId(blockedId) })
      .exec();
  }

  async findBlock(blockerId: string, blockedId: string): Promise<ChatBlockDocument | null> {
    return this.chatBlockModel
      .findOne({ blockerId: new Types.ObjectId(blockerId), blockedId: new Types.ObjectId(blockedId) })
      .exec();
  }

  async findAnyBlockBetween(userAId: string, userBId: string): Promise<ChatBlockDocument | null> {
    return this.chatBlockModel
      .findOne({
        $or: [
          { blockerId: new Types.ObjectId(userAId), blockedId: new Types.ObjectId(userBId) },
          { blockerId: new Types.ObjectId(userBId), blockedId: new Types.ObjectId(userAId) },
        ],
      })
      .exec();
  }

  async findBlocksByBlocker(blockerId: string): Promise<ChatBlockDocument[]> {
    return this.chatBlockModel
      .find({ blockerId: new Types.ObjectId(blockerId) })
      .populate({ path: 'blockedId', select: 'name username stageName avatar isVerified isDisabled' })
      .sort({ createdAt: -1 })
      .exec();
  }
}
