import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { ClientSession, FilterQuery, Model, Types } from 'mongoose';
import { FollowRequest, FollowRequestDocument } from './schemas/follow-request.schema';
import { Follow, FollowDocument } from './schemas/follow.schema';

@Injectable()
export class FollowsRepository {
  constructor(
    @InjectModel(Follow.name) private readonly followModel: Model<FollowDocument>,
    @InjectModel(FollowRequest.name)
    private readonly followRequestModel: Model<FollowRequestDocument>,
  ) {}

  async findOne(
    followerId: string,
    followingId: string,
    session?: ClientSession,
  ): Promise<FollowDocument | null> {
    return this.followModel
      .findOne({
        followerId: new Types.ObjectId(followerId),
        followingId: new Types.ObjectId(followingId),
      })
      .session(session ?? null)
      .exec();
  }

  async create(
    followerId: string,
    followingId: string,
    session?: ClientSession,
  ): Promise<FollowDocument> {
    const [follow] = await this.followModel.create(
      [
        {
          followerId: new Types.ObjectId(followerId),
          followingId: new Types.ObjectId(followingId),
        },
      ],
      { session },
    );

    return follow;
  }

  isDuplicateKeyError(error: unknown): boolean {
    return (
      typeof error === 'object' &&
      error !== null &&
      'code' in error &&
      (error as { code?: number }).code === 11000
    );
  }

  async deleteById(id: string, session?: ClientSession): Promise<void> {
    await this.followModel.findByIdAndDelete(id, { session }).exec();
  }

  async deleteRelationship(
    followerId: string,
    followingId: string,
    session?: ClientSession,
  ): Promise<boolean> {
    const result = await this.followModel
      .deleteOne(
        {
          followerId: new Types.ObjectId(followerId),
          followingId: new Types.ObjectId(followingId),
        },
        { session },
      )
      .exec();
    return (result.deletedCount ?? 0) > 0;
  }

  async findMany(
    filter: FilterQuery<FollowDocument>,
    skip: number,
    limit: number,
    sort: Record<string, 1 | -1> = { createdAt: -1 },
  ): Promise<FollowDocument[]> {
    return this.followModel
      .find(filter)
      .populate({ path: 'followerId', select: 'name username stageName avatar isVerified isDisabled' })
      .populate({ path: 'followingId', select: 'name username stageName avatar isVerified isDisabled' })
      .sort(sort)
      .skip(skip)
      .limit(limit)
      .exec();
  }

  async count(filter: FilterQuery<FollowDocument>): Promise<number> {
    return this.followModel.countDocuments(filter).exec();
  }

  async findFollowingIds(followerId: string): Promise<string[]> {
    const rows = await this.followModel
      .find({ followerId: new Types.ObjectId(followerId) })
      .select({ followingId: 1 })
      .lean()
      .exec();

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

  async findPendingRequest(requesterId: string, targetUserId: string): Promise<FollowRequestDocument | null> {
    return this.followRequestModel
      .findOne({
        requesterId: new Types.ObjectId(requesterId),
        targetUserId: new Types.ObjectId(targetUserId),
        status: 'pending',
      })
      .exec();
  }

  async runInTransaction<T>(
    operation: (session?: ClientSession) => Promise<T>,
    required: boolean,
  ): Promise<T> {
    const session = await this.followModel.db.startSession();
    try {
      let result: T | undefined;
      try {
        await session.withTransaction(async () => {
          result = await operation(session);
        });
        return result as T;
      } catch (error) {
        if (required || !this.isTransactionUnsupported(error)) throw error;
        return operation(undefined);
      }
    } finally {
      await session.endSession();
    }
  }

  async deletePendingRequest(requesterId: string, targetUserId: string): Promise<void> {
    await this.followRequestModel
      .deleteOne({
        requesterId: new Types.ObjectId(requesterId),
        targetUserId: new Types.ObjectId(targetUserId),
        status: 'pending',
      })
      .exec();
  }

  async findFollowerUserIds(followingId: string, skip: number, limit: number, sort: Record<string, 1 | -1>) {
    return this.followModel
      .find({ followingId: new Types.ObjectId(followingId) })
      .select({ followerId: 1 })
      .sort(sort)
      .skip(skip)
      .limit(limit)
      .lean()
      .exec();
  }

  async findFollowingUserIds(followerId: string, skip: number, limit: number, sort: Record<string, 1 | -1>) {
    return this.followModel
      .find({ followerId: new Types.ObjectId(followerId) })
      .select({ followingId: 1 })
      .sort(sort)
      .skip(skip)
      .limit(limit)
      .lean()
      .exec();
  }

  async upsertPendingRequest(requesterId: string, targetUserId: string): Promise<FollowRequestDocument> {
    return this.followRequestModel
      .findOneAndUpdate(
        {
          requesterId: new Types.ObjectId(requesterId),
          targetUserId: new Types.ObjectId(targetUserId),
        },
        {
          requesterId: new Types.ObjectId(requesterId),
          targetUserId: new Types.ObjectId(targetUserId),
          status: 'pending',
        },
        { new: true, upsert: true },
      )
      .exec();
  }

  async updateRequestStatus(
    requestId: string,
    targetUserId: string,
    status: 'approved' | 'rejected',
    session?: ClientSession,
  ): Promise<FollowRequestDocument | null> {
    return this.followRequestModel
      .findOneAndUpdate(
        { _id: new Types.ObjectId(requestId), targetUserId: new Types.ObjectId(targetUserId), status: 'pending' },
        { status },
        { new: true, session },
      )
      .exec();
  }

  async findRequestByIdForTarget(
    requestId: string,
    targetUserId: string,
    session?: ClientSession,
  ): Promise<FollowRequestDocument | null> {
    return this.followRequestModel
      .findOne({ _id: new Types.ObjectId(requestId), targetUserId: new Types.ObjectId(targetUserId) })
      .session(session ?? null)
      .exec();
  }

  private isTransactionUnsupported(error: unknown): boolean {
    const message = error instanceof Error ? error.message : String(error);
    return /Transaction numbers are only allowed|replica set member or mongos/i.test(message);
  }

  async findPendingRequestsForTarget(
    targetUserId: string,
    skip: number,
    limit: number,
    sort: Record<string, 1 | -1> = { createdAt: -1 },
  ): Promise<FollowRequestDocument[]> {
    return this.followRequestModel
      .find({ targetUserId: new Types.ObjectId(targetUserId), status: 'pending' })
      .populate({ path: 'requesterId', select: 'name username stageName avatar isVerified' })
      .sort(sort)
      .skip(skip)
      .limit(limit)
      .exec();
  }

  async countPendingRequestsForTarget(targetUserId: string): Promise<number> {
    return this.followRequestModel
      .countDocuments({ targetUserId: new Types.ObjectId(targetUserId), status: 'pending' })
      .exec();
  }
}
