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 { Post, PostDocument } from './schemas/post.schema';

@Injectable()
export class PostsRepository {
  constructor(@InjectModel(Post.name) private readonly postModel: Model<PostDocument>) {}

  private withActiveFilter<T extends FilterQuery<PostDocument>>(filter: T): FilterQuery<PostDocument> {
    return {
      ...filter,
      isDeleted: { $ne: true },
      ...(Object.prototype.hasOwnProperty.call(filter, 'isArchived')
        ? {}
        : { isArchived: { $ne: true } }),
      moderationStatus: { $ne: ModerationStatus.HIDDEN },
    };
  }

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

  async create(authorId: string, payload: Partial<Post>): Promise<PostDocument> {
    return this.postModel.create({
      ...payload,
      authorId: new Types.ObjectId(authorId),
    });
  }

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

    return this.postModel
      .findOne({ _id: new Types.ObjectId(postId), isDeleted: { $ne: true } })
      .populate({ path: 'authorId', select: 'name username avatar isVerified stageName' })
      .populate({ path: 'taggedUserIds', select: 'name username avatar stageName isVerified' })
      .populate({ path: 'mentionedUserIds', select: 'name username avatar stageName isVerified' })
      .populate({ path: 'collaboratorIds', select: 'name username avatar stageName isVerified' })
      .populate({
        path: 'repostOfPostId quoteOfPostId',
        populate: { path: 'authorId', select: 'name username avatar isVerified stageName' },
      })
      .exec();
  }

  async updateById(postId: string, payload: UpdateQuery<PostDocument>): Promise<PostDocument | null> {
    if (!Types.ObjectId.isValid(postId)) {
      return null;
    }

    return this.postModel
      .findByIdAndUpdate(postId, payload, { new: true })
      .populate({ path: 'authorId', select: 'name username avatar isVerified stageName' })
      .populate({ path: 'taggedUserIds', select: 'name username avatar stageName isVerified' })
      .populate({ path: 'mentionedUserIds', select: 'name username avatar stageName isVerified' })
      .populate({ path: 'collaboratorIds', select: 'name username avatar stageName isVerified' })
      .populate({
        path: 'repostOfPostId quoteOfPostId',
        populate: { path: 'authorId', select: 'name username avatar isVerified stageName' },
      })
      .exec();
  }

  async deleteById(postId: string, deletedBy?: string): Promise<PostDocument | null> {
    if (!Types.ObjectId.isValid(postId)) {
      return null;
    }

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

    return this.postModel
      .findOneAndUpdate(
        { _id: new Types.ObjectId(postId), isDeleted: { $ne: true } },
        { isDeleted: true, deletedAt: new Date(), deletedBy: deletedByObjectId },
        { new: true },
      )
      .exec();
  }

  async findMany(
    filter: FilterQuery<PostDocument>,
    skip: number,
    limit: number,
    sort: Record<string, 1 | -1> = { createdAt: -1 },
  ): Promise<PostDocument[]> {
    return this.postModel
      .find(this.withActiveFilter(filter))
      .populate({ path: 'authorId', select: 'name username avatar isVerified stageName' })
      .populate({ path: 'taggedUserIds', select: 'name username avatar stageName isVerified' })
      .populate({ path: 'mentionedUserIds', select: 'name username avatar stageName isVerified' })
      .populate({ path: 'collaboratorIds', select: 'name username avatar stageName isVerified' })
      .populate({
        path: 'repostOfPostId quoteOfPostId',
        populate: { path: 'authorId', select: 'name username avatar isVerified stageName' },
      })
      .sort(sort)
      .skip(skip)
      .limit(limit)
      .exec();
  }

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

  async findManyByIds(postIds: string[]): Promise<PostDocument[]> {
    if (!postIds.length) {
      return [];
    }

    const ids = postIds.map((id) => new Types.ObjectId(id));
    const rows = await this.postModel
      .find({ _id: { $in: ids }, isDeleted: { $ne: true } })
      .populate({ path: 'authorId', select: 'name username avatar isVerified stageName' })
      .populate({ path: 'taggedUserIds', select: 'name username avatar stageName isVerified' })
      .populate({ path: 'mentionedUserIds', select: 'name username avatar stageName isVerified' })
      .populate({ path: 'collaboratorIds', select: 'name username avatar stageName isVerified' })
      .populate({
        path: 'repostOfPostId quoteOfPostId',
        populate: { path: 'authorId', select: 'name username avatar isVerified stageName' },
      })
      .exec();

    const order = new Map(postIds.map((id, idx) => [id, idx]));
    return rows.sort((a, b) => (order.get(a.id) ?? 0) - (order.get(b.id) ?? 0));
  }

  async incrementLikesCount(postId: string, delta: 1 | -1, session?: ClientSession): Promise<void> {
    await this.incrementNonNegativeCounter(postId, 'likesCount', delta, session);
  }

  async incrementCommentsCount(
    postId: string,
    delta: 1 | -1,
    session?: ClientSession,
  ): Promise<void> {
    await this.incrementNonNegativeCounter(postId, 'commentsCount', delta, session);
  }

  async incrementSavesCount(postId: string, delta: 1 | -1, session?: ClientSession): Promise<void> {
    await this.incrementNonNegativeCounter(postId, 'savesCount', delta, session);
  }

  async incrementShareCount(postId: string, delta = 1, session?: ClientSession): Promise<void> {
    await this.postModel
      .findByIdAndUpdate(postId, { $inc: { shareCount: delta } }, { new: false, session })
      .exec();
  }

  async incrementViewCount(postId: string, delta = 1, session?: ClientSession): Promise<void> {
    await this.postModel
      .findByIdAndUpdate(postId, { $inc: { viewCount: delta } }, { new: false, session })
      .exec();
  }

  async incrementPlayCount(postId: string, delta = 1, session?: ClientSession): Promise<void> {
    await this.postModel
      .findByIdAndUpdate(postId, { $inc: { playCount: delta } }, { new: false, session })
      .exec();
  }

  async setCommentsCount(postId: string, nextValue: number, session?: ClientSession): Promise<void> {
    await this.postModel
      .findByIdAndUpdate(
        postId,
        {
          commentsCount: Math.max(0, nextValue),
        },
        { new: false, session },
      )
      .exec();
  }

  private async incrementNonNegativeCounter(
    postId: string,
    field: 'likesCount' | 'commentsCount' | 'savesCount',
    delta: 1 | -1,
    session?: ClientSession,
  ): Promise<void> {
    await this.postModel
      .findByIdAndUpdate(
        postId,
        [{ $set: { [field]: { $max: [0, { $add: [{ $ifNull: [`$${field}`, 0] }, delta] }] } } }],
        { new: false, session },
      )
      .exec();
  }

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

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

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

    return this.postModel
      .findByIdAndUpdate(
        postId,
        {
          moderationStatus: payload.moderationStatus,
          moderationReason: payload.moderationReason,
        },
        { new: true },
      )
      .populate({ path: 'authorId', select: 'name username avatar isVerified stageName' })
      .populate({ path: 'taggedUserIds', select: 'name username avatar stageName isVerified' })
      .populate({ path: 'mentionedUserIds', select: 'name username avatar stageName isVerified' })
      .populate({ path: 'collaboratorIds', select: 'name username avatar stageName isVerified' })
      .exec();
  }
}
