import {
  BadRequestException,
  ForbiddenException,
  Injectable,
  Logger,
  NotFoundException,
} from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Model, Types } from 'mongoose';
import { PaginationQueryDto } from '../../common/dto/pagination-query.dto';
import { assertAllowedMediaFile, MEDIA_MAX_SIZE_BYTES } from '../../common/media/allowed-media';
import { buildPaginatedResponse } from '../../common/utils/pagination.util';
import { ManagedStorageService } from '../../infrastructure/storage/managed-storage.service';
import { BlocksRepository } from '../blocks/blocks.repository';
import { NotificationsService } from '../notifications/notifications.service';
import { PostsRepository } from '../posts/posts.repository';
import { UsersRepository } from '../users/users.repository';
import { CreateCollaborationRequestDto } from './dto/create-collaboration-request.dto';
import { CreateGeneralCollaborationRequestDto } from './dto/create-general-collaboration-request.dto';
import { CollaborationRequestQueryDto } from './dto/collaboration-request-query.dto';
import {
  CollaborationRequest,
  CollaborationRequestDocument,
  CollaborationRequestStatus,
} from './schemas/collaboration-request.schema';

type UploadedAudioFile = {
  buffer: Buffer;
  originalname: string;
  mimetype: string;
  size: number;
};

@Injectable()
export class CollaborationRequestsService {
  private readonly logger = new Logger(CollaborationRequestsService.name);
  private readonly userSelect = 'name username stageName avatar isVerified isDisabled';

  constructor(
    @InjectModel(CollaborationRequest.name)
    private readonly collaborationRequestModel: Model<CollaborationRequestDocument>,
    private readonly postsRepository: PostsRepository,
    private readonly usersRepository: UsersRepository,
    private readonly blocksRepository: BlocksRepository,
    private readonly notificationsService: NotificationsService,
    private readonly storageService: ManagedStorageService,
  ) {}

  async create(
    requesterId: string,
    postId: string,
    dto: CreateCollaborationRequestDto,
    file?: UploadedAudioFile,
  ) {
    if (!Types.ObjectId.isValid(postId)) {
      throw new BadRequestException('Invalid collaboration request');
    }

    const post = await this.postsRepository.findById(postId);

    if (!post) {
      throw new NotFoundException('Post not found');
    }

    const targetUserId = this.extractEntityId(post.authorId);
    if (!Types.ObjectId.isValid(targetUserId)) {
      throw new BadRequestException('Post owner is invalid');
    }

    if (requesterId === targetUserId) {
      throw new BadRequestException('You cannot request collaboration on your own post');
    }

    const [targetUser, block] = await Promise.all([
      this.usersRepository.findById(targetUserId),
      this.blocksRepository.findAnyBetween(requesterId, targetUserId),
    ]);

    if (!targetUser || targetUser.isDisabled) {
      throw new NotFoundException('Target user not found');
    }

    if (block) {
      throw new BadRequestException('You cannot invite this user');
    }

    const filter = {
      postId: new Types.ObjectId(postId),
      requesterId: new Types.ObjectId(requesterId),
      targetUserId: new Types.ObjectId(targetUserId),
      status: 'pending',
    };

    const collaborationDetails = await this.buildCollaborationDetailsWithFile(dto, file);
    const existing = await this.collaborationRequestModel.findOne(filter).exec();

    const request = existing
      ? await this.updateExistingPendingRequest(existing.id, collaborationDetails)
      : await this.collaborationRequestModel
          .findOneAndUpdate(
            filter,
            {
              $setOnInsert: {
                postId: new Types.ObjectId(postId),
                requesterId: new Types.ObjectId(requesterId),
                targetUserId: new Types.ObjectId(targetUserId),
                status: 'pending',
                ...collaborationDetails,
              },
            },
            { new: true, upsert: true, setDefaultsOnInsert: true },
          )
          .exec();

    if (!request) {
      throw new NotFoundException('Collaboration request not found');
    }

    if (!existing) {
      await this.createCollaborationNotification({
        actorId: requesterId,
        recipientId: targetUserId,
        type: 'collaboration_request',
        referenceId: postId,
        resourceType: 'post',
        deepLink: `/posts/${postId}`,
        requestId: request.id,
        metadata: collaborationDetails,
      });
    }

    return {
      message: 'Collaboration request sent',
      request: await this.findVisibleRequestOrFail(requesterId, request.id),
    };
  }

  async createGeneral(
    requesterId: string,
    targetUserId: string,
    dto: CreateGeneralCollaborationRequestDto,
    file?: UploadedAudioFile,
  ) {
    if (!Types.ObjectId.isValid(targetUserId)) {
      throw new BadRequestException('Invalid collaboration request');
    }

    if (requesterId === targetUserId) {
      throw new BadRequestException('You cannot invite yourself');
    }

    const [targetUser, block] = await Promise.all([
      this.usersRepository.findById(targetUserId),
      this.blocksRepository.findAnyBetween(requesterId, targetUserId),
    ]);

    if (!targetUser || targetUser.isDisabled) {
      throw new NotFoundException('Target user not found');
    }

    if (block) {
      throw new BadRequestException('You cannot invite this user');
    }

    const filter = {
      postId: null,
      requesterId: new Types.ObjectId(requesterId),
      targetUserId: new Types.ObjectId(targetUserId),
      status: 'pending',
    };

    const collaborationDetails = await this.buildCollaborationDetailsWithFile(dto, file);
    const existing = await this.collaborationRequestModel.findOne(filter).exec();

    const request = existing
      ? await this.updateExistingPendingRequest(existing.id, collaborationDetails)
      : await this.collaborationRequestModel
          .findOneAndUpdate(
            filter,
            {
              $setOnInsert: {
                postId: null,
                requesterId: new Types.ObjectId(requesterId),
                targetUserId: new Types.ObjectId(targetUserId),
                status: 'pending',
                ...collaborationDetails,
              },
            },
            { new: true, upsert: true, setDefaultsOnInsert: true },
          )
          .exec();

    if (!request) {
      throw new NotFoundException('Collaboration request not found');
    }

    if (!existing) {
      await this.createCollaborationNotification({
        actorId: requesterId,
        recipientId: targetUserId,
        type: 'collaboration_request',
        referenceId: requesterId,
        resourceType: 'user',
        deepLink: `/users/${requesterId}`,
        requestId: request.id,
        metadata: collaborationDetails,
      });
    }

    return {
      message: 'Collaboration request sent',
      request: await this.findVisibleRequestOrFail(requesterId, request.id),
    };
  }

  async getMine(targetUserId: string, query: PaginationQueryDto) {
    return this.listReceived(targetUserId, { ...query, status: 'pending' });
  }

  async listReceived(targetUserId: string, query: CollaborationRequestQueryDto) {
    return this.listForUser('targetUserId', targetUserId, query);
  }

  async listSent(requesterId: string, query: CollaborationRequestQueryDto) {
    return this.listForUser('requesterId', requesterId, query);
  }

  async getById(currentUserId: string, requestId: string) {
    return {
      request: await this.findVisibleRequestOrFail(currentUserId, requestId),
    };
  }

  async cancel(requesterId: string, requestId: string) {
    if (!Types.ObjectId.isValid(requestId)) {
      throw new BadRequestException('Invalid collaboration request id');
    }

    const request = await this.collaborationRequestModel.findById(requestId).exec();

    if (!request) {
      throw new NotFoundException('Collaboration request not found');
    }

    if (request.requesterId.toString() !== requesterId) {
      throw new ForbiddenException('Only the requester can cancel this collaboration request');
    }

    if (request.status !== 'pending') {
      throw new BadRequestException('Only pending collaboration requests can be cancelled');
    }

    request.status = 'cancelled';
    await request.save();

    await this.createCollaborationNotification({
      actorId: requesterId,
      recipientId: request.targetUserId.toString(),
      type: 'collaboration_request_cancelled',
      ...this.buildNotificationTargetForRequest(request, requesterId),
      requestId: request.id,
      metadata: {
        status: 'cancelled',
        collaborationType: request.collaborationType ?? null,
      },
    });

    return {
      cancelled: true,
      request: await this.findVisibleRequestOrFail(requesterId, request.id),
    };
  }

  private async listForUser(
    field: 'targetUserId' | 'requesterId',
    userId: string,
    query: CollaborationRequestQueryDto,
  ) {
    const page = query.page ?? 1;
    const limit = query.limit ?? 20;
    const skip = (page - 1) * limit;

    const filter: Record<string, unknown> = {
      [field]: new Types.ObjectId(userId),
      ...(query.status ? { status: query.status } : {}),
    };

    const [items, total] = await Promise.all([
      this.populateRequestQuery(this.collaborationRequestModel.find(filter))
        .sort({ createdAt: -1 })
        .skip(skip)
        .limit(limit)
        .exec(),
      this.collaborationRequestModel.countDocuments(filter).exec(),
    ]);

    return buildPaginatedResponse(
      items.map((item: CollaborationRequestDocument) => this.serializeRequest(item)),
      { page, limit, total, offset: skip },
    );
  }

  async approve(targetUserId: string, requestId: string) {
    const request = await this.updateStatus(targetUserId, requestId, 'approved');

    if (request.postId) {
      await this.postsRepository.updateById(request.postId.toString(), {
        $addToSet: { collaboratorIds: request.requesterId },
      });
    }

    await this.createCollaborationNotification({
      actorId: targetUserId,
      recipientId: request.requesterId.toString(),
      type: 'collaboration_request_approved',
      ...this.buildNotificationTargetForRequest(request, targetUserId),
      requestId: request.id,
      metadata: {
        status: 'approved',
        collaborationType: request.collaborationType ?? null,
      },
    });

    return {
      approved: true,
      request: await this.findVisibleRequestOrFail(targetUserId, request.id),
    };
  }

  async reject(targetUserId: string, requestId: string) {
    const request = await this.updateStatus(targetUserId, requestId, 'rejected');

    await this.createCollaborationNotification({
      actorId: targetUserId,
      recipientId: request.requesterId.toString(),
      type: 'collaboration_request_rejected',
      ...this.buildNotificationTargetForRequest(request, targetUserId),
      requestId: request.id,
      metadata: {
        status: 'rejected',
        collaborationType: request.collaborationType ?? null,
      },
    });

    return {
      rejected: true,
      request: await this.findVisibleRequestOrFail(targetUserId, request.id),
    };
  }

  private buildCollaborationDetails(
    dto: CreateCollaborationRequestDto | CreateGeneralCollaborationRequestDto,
  ) {
    const attachmentUrl = typeof dto.attachmentUrl === 'string' ? dto.attachmentUrl.trim() : '';
    return {
      ...(dto.collaborationType ? { collaborationType: dto.collaborationType } : {}),
      ...(typeof dto.message === 'string' ? { message: dto.message.trim() } : {}),
      ...(attachmentUrl ? { attachmentUrl } : {}),
      ...(dto.attachmentType ? { attachmentType: dto.attachmentType } : {}),
    };
  }

  private async buildCollaborationDetailsWithFile(
    dto: CreateCollaborationRequestDto | CreateGeneralCollaborationRequestDto,
    file?: UploadedAudioFile,
  ) {
    const collaborationDetails = this.buildCollaborationDetails(dto);

    if (!file) {
      return collaborationDetails;
    }

    const attachmentUrl = await this.saveCollaborationAttachment(file);

    return {
      ...collaborationDetails,
      attachmentUrl,
      attachmentType: dto.attachmentType ?? 'audio',
    };
  }

  private async saveCollaborationAttachment(file: UploadedAudioFile): Promise<string> {
    const extension = assertAllowedMediaFile(file, 'audio', {
      invalidMessage: 'Only audio files are allowed for collaboration attachments',
      maxSizeBytes: MEDIA_MAX_SIZE_BYTES.collaborationAudio,
      maxSizeMessage: 'Audio attachment must be less than 20MB',
    });

    if (!file.buffer || !file.buffer.length) {
      throw new BadRequestException('Invalid audio attachment');
    }

    return this.storageService.saveFile({
      folderSegments: ['collaboration', 'audio'],
      extension,
      buffer: file.buffer,
      contentType: file.mimetype,
      fileNamePrefix: 'collaboration',
    });
  }

  private async updateExistingPendingRequest(
    requestId: string,
    collaborationDetails: Record<string, unknown>,
  ) {
    if (!Object.keys(collaborationDetails).length) {
      return this.collaborationRequestModel.findById(requestId).exec();
    }

    return this.collaborationRequestModel
      .findByIdAndUpdate(requestId, { $set: collaborationDetails }, { new: true })
      .exec();
  }

  private async updateStatus(
    targetUserId: string,
    requestId: string,
    status: Extract<CollaborationRequestStatus, 'approved' | 'rejected'>,
  ) {
    if (!Types.ObjectId.isValid(requestId)) {
      throw new BadRequestException('Invalid collaboration request id');
    }

    const request = await this.collaborationRequestModel.findById(requestId).exec();

    if (!request) {
      throw new NotFoundException('Collaboration request not found');
    }

    if (request.targetUserId.toString() !== targetUserId) {
      throw new ForbiddenException('Only the target user can update this collaboration request');
    }

    if (request.status !== 'pending') {
      throw new BadRequestException('Only pending collaboration requests can be updated');
    }

    request.status = status;
    await request.save();

    return request;
  }

  private populateRequestQuery(query: any) {
    return query
      .populate({ path: 'requesterId', select: this.userSelect })
      .populate({ path: 'targetUserId', select: this.userSelect })
      .populate({ path: 'postId' });
  }

  private async findVisibleRequestOrFail(currentUserId: string, requestId: string) {
    if (!Types.ObjectId.isValid(requestId)) {
      throw new BadRequestException('Invalid collaboration request id');
    }

    const request = await this.populateRequestQuery(
      this.collaborationRequestModel.findOne({
        _id: new Types.ObjectId(requestId),
        $or: [
          { requesterId: new Types.ObjectId(currentUserId) },
          { targetUserId: new Types.ObjectId(currentUserId) },
        ],
      }),
    ).exec();

    if (!request) {
      throw new NotFoundException('Collaboration request not found');
    }

    return this.serializeRequest(request);
  }

  private async createCollaborationNotification(options: {
    actorId: string;
    recipientId: string;
    type:
      | 'collaboration_request'
      | 'collaboration_request_approved'
      | 'collaboration_request_rejected'
      | 'collaboration_request_cancelled';
    referenceId: string;
    resourceType: string;
    deepLink: string;
    requestId: string;
    metadata?: Record<string, unknown>;
  }) {
    try {
      await this.notificationsService.create({
        actorId: options.actorId,
        recipientId: options.recipientId,
        type: options.type,
        referenceId: options.referenceId,
        resourceType: options.resourceType,
        deepLink: options.deepLink,
        metadata: {
          ...(options.metadata ?? {}),
          collaborationRequestId: options.requestId,
          requestId: options.requestId,
          status: options.metadata?.status ?? 'pending',
          postId: options.resourceType === 'post' ? options.referenceId : options.metadata?.postId,
        },
      });
    } catch (error) {
      this.logger.warn(
        `Collaboration notification failed: ${
          error instanceof Error ? error.message : String(error)
        }`,
      );
    }
  }

  private buildNotificationTargetForRequest(
    request: CollaborationRequestDocument,
    actorId: string,
  ) {
    if (request.postId) {
      const postId = request.postId.toString();

      return {
        referenceId: postId,
        resourceType: 'post',
        deepLink: `/posts/${postId}`,
      };
    }

    return {
      referenceId: actorId,
      resourceType: 'user',
      deepLink: `/users/${actorId}`,
    };
  }

  private extractEntityId(value: unknown): string {
    if (!value) {
      return '';
    }
    if (typeof value === 'string') {
      return value;
    }
    if (value instanceof Types.ObjectId) {
      return value.toString();
    }
    if (typeof value === 'object') {
      const candidate = value as { _id?: unknown; id?: unknown };
      if (candidate._id instanceof Types.ObjectId) {
        return candidate._id.toString();
      }
      if (typeof candidate._id === 'string') {
        return candidate._id;
      }
      if (typeof candidate.id === 'string') {
        return candidate.id;
      }
    }
    return '';
  }

  private serializeRequest(request: unknown) {
    const plain = this.toPlainRecord(request);
    const requester = this.serializeUser(plain.requesterId);
    const targetUser = this.serializeUser(plain.targetUserId);
    const post = this.serializePost(plain.postId);
    const postId = this.extractEntityId(plain.postId) || plain.postId;

    return {
      ...plain,
      id: this.extractEntityId(plain) || plain.id,
      requesterId: plain.requesterId,
      targetUserId: plain.targetUserId,
      postId,
      requester,
      targetUser,
      post,
      attachmentUrl: typeof plain.attachmentUrl === 'string' ? plain.attachmentUrl : '',
      attachmentType: plain.attachmentType ?? null,
    };
  }

  private serializeUser(value: unknown) {
    if (!value) {
      return null;
    }

    const plain = this.toPlainRecord(value);
    const id = this.extractEntityId(plain) || this.extractEntityId(value);

    return {
      ...plain,
      _id: plain._id ?? id,
      id,
      username: plain.username ?? '',
      name: plain.name ?? '',
      stageName: plain.stageName ?? '',
      avatar: plain.avatar ?? '',
      isVerified: Boolean(plain.isVerified),
    };
  }

  private serializePost(value: unknown) {
    if (!value) {
      return null;
    }

    const plain = this.toPlainRecord(value);
    const id = this.extractEntityId(plain) || this.extractEntityId(value);

    return {
      ...plain,
      _id: plain._id ?? id,
      id,
    };
  }

  private toPlainRecord(value: unknown): Record<string, any> {
    if (!value) {
      return {};
    }

    if (typeof value === 'string' || value instanceof Types.ObjectId) {
      const id = this.extractEntityId(value);
      return { _id: id, id };
    }

    if (typeof value === 'object') {
      const maybeDocument = value as { toJSON?: () => unknown; toObject?: () => unknown };
      if (typeof maybeDocument.toJSON === 'function') {
        const json = maybeDocument.toJSON();
        if (json && typeof json === 'object') {
          return json as Record<string, any>;
        }
      }
      if (typeof maybeDocument.toObject === 'function') {
        const object = maybeDocument.toObject();
        if (object && typeof object === 'object') {
          return object as Record<string, any>;
        }
      }
      return { ...(value as Record<string, any>) };
    }

    return {};
  }
}
