import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { FilterQuery, Model, Types } from 'mongoose';
import { Block, BlockDocument } from './schemas/block.schema';

@Injectable()
export class BlocksRepository {
  constructor(@InjectModel(Block.name) private readonly blockModel: Model<BlockDocument>) {}

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

  async findAnyBetween(userA: string, userB: string): Promise<BlockDocument | null> {
    return this.blockModel
      .findOne({
        $or: [
          { blockerId: new Types.ObjectId(userA), blockedId: new Types.ObjectId(userB) },
          { blockerId: new Types.ObjectId(userB), blockedId: new Types.ObjectId(userA) },
        ],
      })
      .exec();
  }

  async findBlockedUsers(
    blockerId: string,
    skip: number,
    limit: number,
    search?: string,
  ): Promise<Array<BlockDocument | Record<string, unknown>>> {
    if (search?.trim()) {
      return this.aggregateBlockedUsers(blockerId, skip, limit, search);
    }

    return this.blockModel
      .find({ blockerId: new Types.ObjectId(blockerId) })
      .populate({ path: 'blockedId', select: 'name username avatar stageName isDisabled' })
      .sort({ createdAt: -1 })
      .skip(skip)
      .limit(limit)
      .exec();
  }

  async countBlockedUsers(blockerId: string, search?: string): Promise<number> {
    const trimmedSearch = search?.trim();
    if (!trimmedSearch) {
      return this.blockModel.countDocuments({ blockerId: new Types.ObjectId(blockerId) }).exec();
    }

    const [result] = await this.blockModel
      .aggregate<{ total: number }>([
        { $match: { blockerId: new Types.ObjectId(blockerId) } },
        {
          $lookup: {
            from: 'users',
            localField: 'blockedId',
            foreignField: '_id',
            as: 'blockedUser',
          },
        },
        { $unwind: '$blockedUser' },
        { $match: this.buildBlockedUserSearchMatch(trimmedSearch) },
        { $count: 'total' },
      ])
      .exec();

    return result?.total ?? 0;
  }

  async create(blockerId: string, blockedId: string): Promise<void> {
    await this.blockModel
      .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 remove(blockerId: string, blockedId: string): Promise<void> {
    await this.blockModel
      .deleteOne({
        blockerId: new Types.ObjectId(blockerId),
        blockedId: new Types.ObjectId(blockedId),
      })
      .exec();
  }

  async findBlockedIds(blockerId: string): Promise<string[]> {
    const rows = await this.blockModel
      .find({ blockerId: new Types.ObjectId(blockerId) })
      .select({ blockedId: 1 })
      .lean()
      .exec();

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

  async findBlockingOrBlockedIds(userId: string): Promise<string[]> {
    const userObjectId = new Types.ObjectId(userId);
    const rows = await this.blockModel
      .find({ $or: [{ blockerId: userObjectId }, { blockedId: userObjectId }] })
      .select({ blockerId: 1, blockedId: 1 })
      .lean()
      .exec();

    return Array.from(
      new Set(
        rows.map((row) =>
          row.blockerId.toString() === userId ? row.blockedId.toString() : row.blockerId.toString(),
        ),
      ),
    );
  }

  private async aggregateBlockedUsers(
    blockerId: string,
    skip: number,
    limit: number,
    search: string,
  ): Promise<Record<string, unknown>[]> {
    const trimmedSearch = search.trim();
    return this.blockModel
      .aggregate<Record<string, unknown>>([
        { $match: { blockerId: new Types.ObjectId(blockerId) } },
        {
          $lookup: {
            from: 'users',
            localField: 'blockedId',
            foreignField: '_id',
            as: 'blockedUser',
          },
        },
        { $unwind: '$blockedUser' },
        { $match: this.buildBlockedUserSearchMatch(trimmedSearch) },
        { $sort: { createdAt: -1 } },
        { $skip: skip },
        { $limit: limit },
        {
          $project: {
            blockedId: {
              _id: '$blockedUser._id',
              name: '$blockedUser.name',
              username: '$blockedUser.username',
              avatar: '$blockedUser.avatar',
              stageName: '$blockedUser.stageName',
              isDisabled: '$blockedUser.isDisabled',
            },
            createdAt: 1,
          },
        },
      ])
      .exec();
  }

  private buildBlockedUserSearchMatch(search: string): FilterQuery<BlockDocument> {
    return {
      $or: [
        { 'blockedUser.name': { $regex: search, $options: 'i' } },
        { 'blockedUser.username': { $regex: search, $options: 'i' } },
        { 'blockedUser.stageName': { $regex: search, $options: 'i' } },
      ],
    };
  }
}
