import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { ClientSession, FilterQuery, Model, Types, UpdateQuery } from 'mongoose';
import { ModerationStatus } from '../../common/enums/moderation-status.enum';
import { Like, LikeDocument } from '../likes/schemas/like.schema';
import { Comment, CommentDocument } from './schemas/comment.schema';

@Injectable()
export class CommentsRepository {
  constructor(
    @InjectModel(Comment.name) private readonly commentModel: Model<CommentDocument>,
    @InjectModel(Like.name) private readonly likeModel: Model<LikeDocument>,
  ) {}

  private withActiveFilter<T extends FilterQuery<CommentDocument>>(filter: T): FilterQuery<CommentDocument> {
    return {
      ...filter,
      isDeleted: { $ne: true },
      hiddenByFilter: { $ne: true },
      moderationStatus: { $ne: ModerationStatus.HIDDEN },
    };
  }

  private withAdminFilter<T extends FilterQuery<CommentDocument>>(filter: T): FilterQuery<CommentDocument> {
    return {
      ...filter,
      isDeleted: { $ne: true },
    };
  }

  async create(
    payload: {
      postId: string;
      authorId: string;
      content: string;
      mentionUsernames?: string[];
      parentCommentId?: string;
      hiddenByFilter?: boolean;
      hiddenReason?: string;
    },
    session?: ClientSession,
  ) {
    const doc = new this.commentModel({
      postId: new Types.ObjectId(payload.postId),
      authorId: new Types.ObjectId(payload.authorId),
      content: payload.content,
      mentionUsernames: payload.mentionUsernames ?? [],
      hiddenByFilter: payload.hiddenByFilter ?? false,
      hiddenReason: payload.hiddenReason ?? '',
      ...(payload.parentCommentId ? { parentCommentId: new Types.ObjectId(payload.parentCommentId) } : {}),
    });

    return session ? doc.save({ session }) : doc.save();
  }

  async findById(commentId: string): Promise<CommentDocument | null> {
    if (!Types.ObjectId.isValid(commentId)) {
      return null;
    }

    return this.commentModel
      .findOne({ _id: new Types.ObjectId(commentId), isDeleted: { $ne: true } })
      .exec();
  }

  async findByIdWithAuthor(commentId: string): Promise<CommentDocument | null> {
    if (!Types.ObjectId.isValid(commentId)) {
      return null;
    }

    return this.commentModel
      .findOne({ _id: new Types.ObjectId(commentId), isDeleted: { $ne: true } })
      .populate({ path: 'authorId', select: 'name username avatar stageName isVerified' })
      .exec();
  }

  async deleteById(commentId: string, deletedBy?: string, session?: ClientSession): Promise<boolean> {
    if (!Types.ObjectId.isValid(commentId)) {
      return false;
    }

    const deletedByObjectId =
      deletedBy && Types.ObjectId.isValid(deletedBy) ? new Types.ObjectId(deletedBy) : null;

    const updated = await this.commentModel
      .findOneAndUpdate(
        { _id: new Types.ObjectId(commentId), isDeleted: { $ne: true } },
        { isDeleted: true, deletedAt: new Date(), deletedBy: deletedByObjectId },
        { new: false, session },
      )
      .exec();

    return !!updated;
  }

  async updateById(
    commentId: string,
    payload: UpdateQuery<Pick<Comment, 'content' | 'mentionUsernames'>>,
  ): Promise<CommentDocument | null> {
    if (!Types.ObjectId.isValid(commentId)) {
      return null;
    }

    return this.commentModel
      .findOneAndUpdate({ _id: new Types.ObjectId(commentId), isDeleted: { $ne: true } }, payload, {
        new: true,
      })
      .populate({ path: 'authorId', select: 'name username avatar stageName isVerified' })
      .exec();
  }

  async findMany(
    filter: FilterQuery<CommentDocument>,
    skip: number,
    limit: number,
    sort: Record<string, 1 | -1> = { createdAt: -1 },
  ) {
    return this.commentModel
      .find(this.withActiveFilter(filter))
      .populate({ path: 'authorId', select: 'name username avatar stageName isVerified' })
      .sort(sort)
      .skip(skip)
      .limit(limit)
      .exec();
  }

  async setPinned(commentId: string, isPinned: boolean): Promise<CommentDocument | null> {
    if (!Types.ObjectId.isValid(commentId)) {
      return null;
    }

    return this.commentModel
      .findOneAndUpdate(
        { _id: new Types.ObjectId(commentId), isDeleted: { $ne: true } },
        { isPinned },
        { new: true },
      )
      .populate({ path: 'authorId', select: 'name username avatar stageName isVerified' })
      .exec();
  }

  async findManyTop(
    filter: FilterQuery<CommentDocument>,
    skip: number,
    limit: number,
    createdAtDirection: 1 | -1 = -1,
  ): Promise<CommentDocument[]> {
    const rows = await this.commentModel
      .aggregate<{ _id: Types.ObjectId }>([
        { $match: this.withActiveFilter(filter) },
        {
          $lookup: {
            from: this.likeModel.collection.name,
            let: { commentId: '$_id' },
            pipeline: [
              {
                $match: {
                  $expr: {
                    $and: [
                      { $eq: ['$targetId', '$$commentId'] },
                      { $eq: ['$targetType', 'comment'] },
                    ],
                  },
                },
              },
              { $project: { _id: 1 } },
            ],
            as: 'commentLikes',
          },
        },
        { $addFields: { likesCountForSort: { $size: '$commentLikes' } } },
        { $sort: { likesCountForSort: -1, createdAt: createdAtDirection } },
        { $skip: skip },
        { $limit: limit },
        { $project: { _id: 1 } },
      ])
      .exec();

    const ids = rows.map((row) => row._id);
    if (!ids.length) {
      return [];
    }

    const docs = await this.commentModel
      .find({ _id: { $in: ids } })
      .populate({ path: 'authorId', select: 'name username avatar stageName isVerified' })
      .exec();
    const docsById = new Map(docs.map((doc) => [doc.id, doc]));

    const orderedDocs: CommentDocument[] = [];
    for (const id of ids) {
      const doc = docsById.get(id.toString());
      if (doc) {
        orderedDocs.push(doc);
      }
    }

    return orderedDocs;
  }

  async findManyAdmin(
    filter: FilterQuery<CommentDocument>,
    skip: number,
    limit: number,
    sort: Record<string, 1 | -1> = { createdAt: -1 },
  ) {
    return this.commentModel
      .find(this.withAdminFilter(filter))
      .populate({ path: 'authorId', select: 'name username avatar stageName isVerified' })
      .sort(sort)
      .skip(skip)
      .limit(limit)
      .exec();
  }

  async count(filter: FilterQuery<CommentDocument>): Promise<number> {
    return this.commentModel.countDocuments(this.withActiveFilter(filter)).exec();
  }

  async countRepliesByParentIds(parentCommentIds: string[]): Promise<Record<string, number>> {
    if (!parentCommentIds.length) {
      return {};
    }

    const rows = await this.commentModel
      .aggregate<{ _id: Types.ObjectId; count: number }>([
        {
          $match: this.withActiveFilter({
            parentCommentId: { $in: parentCommentIds.map((id) => new Types.ObjectId(id)) },
          }),
        },
        { $group: { _id: '$parentCommentId', count: { $sum: 1 } } },
      ])
      .exec();

    return Object.fromEntries(rows.map((row) => [row._id.toString(), row.count]));
  }

  async findReplyPreviewsByParentIds(
    parentCommentIds: string[],
    limitPerParent = 2,
  ): Promise<Record<string, CommentDocument[]>> {
    if (!parentCommentIds.length) {
      return {};
    }

    const previews: Record<string, CommentDocument[]> = {};
    await Promise.all(
      parentCommentIds.map(async (parentCommentId) => {
        previews[parentCommentId] = await this.findMany(
          { parentCommentId: new Types.ObjectId(parentCommentId) },
          0,
          limitPerParent,
          { createdAt: 1 },
        );
      }),
    );

    return previews;
  }

  async countLikesByCommentIds(commentIds: string[]): Promise<Record<string, number>> {
    if (!commentIds.length) {
      return {};
    }

    const rows = await this.likeModel
      .aggregate<{ _id: Types.ObjectId; count: number }>([
        {
          $match: {
            targetType: 'comment',
            targetId: { $in: commentIds.map((id) => new Types.ObjectId(id)) },
          },
        },
        { $group: { _id: '$targetId', count: { $sum: 1 } } },
      ])
      .exec();

    return Object.fromEntries(rows.map((row) => [row._id.toString(), row.count]));
  }

  async findLikedCommentIds(userId: string, commentIds: string[]): Promise<string[]> {
    if (!commentIds.length || !Types.ObjectId.isValid(userId)) {
      return [];
    }

    const rows = await this.likeModel
      .find({
        userId: new Types.ObjectId(userId),
        targetType: 'comment',
        targetId: { $in: commentIds.map((id) => new Types.ObjectId(id)) },
      })
      .select({ targetId: 1 })
      .lean()
      .exec();

    return rows.map((row) => row.targetId.toString());
  }

  async countAdmin(filter: FilterQuery<CommentDocument>): Promise<number> {
    return this.commentModel.countDocuments(this.withAdminFilter(filter)).exec();
  }

  async updateModerationStatus(
    commentId: string,
    payload: Pick<Comment, 'moderationStatus' | 'moderationReason'>,
  ): Promise<CommentDocument | null> {
    if (!Types.ObjectId.isValid(commentId)) {
      return null;
    }

    return this.commentModel
      .findByIdAndUpdate(
        commentId,
        {
          moderationStatus: payload.moderationStatus,
          moderationReason: payload.moderationReason,
        },
        { new: true },
      )
      .populate({ path: 'authorId', select: 'name username avatar stageName isVerified' })
      .exec();
  }

  async countByPost(postId: string): Promise<number> {
    return this.commentModel
      .countDocuments({ postId: new Types.ObjectId(postId), isDeleted: { $ne: true } })
      .exec();
  }
}
