import { BadRequestException, ForbiddenException, Injectable, Logger, NotFoundException } from '@nestjs/common';
import { Types } from 'mongoose';
import { ModerationStatus } from '../../common/enums/moderation-status.enum';
import { buildPaginatedResponse } from '../../common/utils/pagination.util';
import { resolveMongoSortDirection } from '../../common/utils/sort.util';
import { escapeRegex } from '../../common/utils/regex.util';
import { FeedVersionService } from '../../infrastructure/cache/feed-version.service';
import { AuditService } from '../audit/audit.service';
import { NotificationsService } from '../notifications/notifications.service';
import { FollowsRepository } from '../follows/follows.repository';
import { PostsRepository } from '../posts/posts.repository';
import { UsersRepository } from '../users/users.repository';
import { BlocksRepository } from '../blocks/blocks.repository';
import { AdminCommentQueryDto } from './dto/admin-comment-query.dto';
import { CommentQueryDto, CommentSortBy } from './dto/comment-query.dto';
import { CreateCommentDto } from './dto/create-comment.dto';
import { CreateCommentReplyDto } from './dto/create-comment-reply.dto';
import { UpdateCommentDto } from './dto/update-comment.dto';
import { CommentsRepository } from './comments.repository';
import { CommentDocument } from './schemas/comment.schema';

type CommentAuthorSummary = {
  id: string;
  name?: string;
  username?: string;
  avatar?: string;
  stageName?: string;
  isVerified?: boolean;
};

type InstagramComment = Record<string, unknown> & {
  repliesCount: number;
  repliesPreview: InstagramComment[];
  likesCount: number;
  likedByMe: boolean;
  canEdit: boolean;
  canDelete: boolean;
  replyToUser: CommentAuthorSummary | null;
};

@Injectable()
export class CommentsService {
  private readonly logger = new Logger(CommentsService.name);

  constructor(
    private readonly commentsRepository: CommentsRepository,
    private readonly postsRepository: PostsRepository,
    private readonly auditService: AuditService,
    private readonly feedVersionService: FeedVersionService,
    private readonly notificationsService: NotificationsService,
    private readonly usersRepository: UsersRepository,
    private readonly followsRepository: FollowsRepository,
    private readonly blocksRepository: BlocksRepository,
  ) {}

  async create(userId: string, dto: CreateCommentDto) {
    const post = await this.postsRepository.findById(dto.postId);
    if (!post) {
      throw new NotFoundException('Post not found');
    }
    await this.assertCanComment(userId, post);

    let parentRecipientId = '';
    if (dto.parentCommentId) {
      const parent = await this.commentsRepository.findById(dto.parentCommentId);
      if (!parent || parent.postId.toString() !== dto.postId) {
        throw new NotFoundException('Parent comment not found');
      }
      parentRecipientId = parent.authorId.toString();
      await this.assertNoBlockBetween(userId, parentRecipientId);
    }

    const content = dto.content.trim();
    const hiddenByFilter = this.matchesCommentFilter(content, post.commentFilterKeywords ?? []);
    const mentionResolution = await this.resolveMentionTargets(dto.mentionUsernames, content, userId);
    const comment = await this.commentsRepository.create({
      postId: dto.postId,
      authorId: userId,
      content,
      mentionUsernames: mentionResolution.mentionUsernames,
      parentCommentId: dto.parentCommentId,
      hiddenByFilter,
      hiddenReason: hiddenByFilter ? 'keyword_filter' : '',
    });
    await this.syncCommentsCount(dto.postId);
    await this.feedVersionService.bumpUserVersion(userId);
    const postAuthorId = this.extractEntityId(post.authorId);
    const previewText = content.slice(0, 160);
    const commentNotificationRecipients = await this.dispatchCommentNotifications(
      userId,
      postAuthorId,
      parentRecipientId,
      dto.postId,
      previewText,
    );
    await this.notifyMentionedUsers(
      userId,
      dto.postId,
      mentionResolution.mentionedUsers,
      previewText,
      commentNotificationRecipients,
    );
    return comment;
  }

  async pin(userId: string, commentId: string) {
    const comment = await this.commentsRepository.findById(commentId);
    if (!comment) {
      throw new NotFoundException('Comment not found');
    }
    const post = await this.postsRepository.findById(comment.postId.toString());
    if (!post) {
      throw new NotFoundException('Post not found');
    }
    if (this.extractEntityId(post.authorId) !== userId) {
      throw new ForbiddenException('Only the post owner can pin comments');
    }
    const updated = await this.commentsRepository.setPinned(commentId, true);
    if (!updated) {
      throw new NotFoundException('Comment not found');
    }
    return updated;
  }

  async unpin(userId: string, commentId: string) {
    const comment = await this.commentsRepository.findById(commentId);
    if (!comment) {
      throw new NotFoundException('Comment not found');
    }
    const post = await this.postsRepository.findById(comment.postId.toString());
    if (!post) {
      throw new NotFoundException('Post not found');
    }
    if (this.extractEntityId(post.authorId) !== userId) {
      throw new ForbiddenException('Only the post owner can unpin comments');
    }
    const updated = await this.commentsRepository.setPinned(commentId, false);
    if (!updated) {
      throw new NotFoundException('Comment not found');
    }
    return updated;
  }

  async createReply(userId: string, parentCommentId: string, dto: CreateCommentReplyDto) {
    const parent = await this.commentsRepository.findById(parentCommentId);
    if (!parent) {
      throw new NotFoundException('Parent comment not found');
    }

    return this.create(userId, {
      postId: parent.postId.toString(),
      content: dto.content,
      mentionUsernames: dto.mentionUsernames,
      parentCommentId,
    });
  }

  async remove(userId: string, commentId: string) {
    const comment = await this.commentsRepository.findById(commentId);
    if (!comment) {
      throw new NotFoundException('Comment not found');
    }

    if (comment.authorId.toString() !== userId) {
      throw new ForbiddenException('You can only delete your own comments');
    }

    await this.commentsRepository.deleteById(commentId, userId);
    await this.syncCommentsCount(comment.postId.toString());
    await this.feedVersionService.bumpUserVersion(userId);
    return { success: true };
  }

  async update(userId: string, commentId: string, dto: UpdateCommentDto) {
    const comment = await this.commentsRepository.findById(commentId);
    if (!comment) {
      throw new NotFoundException('Comment not found');
    }

    if (comment.authorId.toString() !== userId) {
      throw new ForbiddenException('You can only update your own comments');
    }

    const hasContentUpdate = typeof dto.content === 'string';
    const hasMentionUpdate = typeof dto.mentionUsernames !== 'undefined';
    if (!hasContentUpdate && !hasMentionUpdate) {
      throw new BadRequestException('Nothing to update');
    }

    const nextContent = hasContentUpdate ? dto.content!.trim() : comment.content;
    if (!nextContent) {
      throw new BadRequestException('Comment content cannot be empty');
    }

    const previousMentionUsernames = this.normalizeMentionUsernames(comment.mentionUsernames ?? []);
    const mentionResolution = await this.resolveMentionTargets(dto.mentionUsernames, nextContent, userId);
    const updated = await this.commentsRepository.updateById(commentId, {
      content: nextContent,
      mentionUsernames: mentionResolution.mentionUsernames,
    });
    if (!updated) {
      throw new NotFoundException('Comment not found');
    }

    await this.feedVersionService.bumpUserVersion(userId);

    const previousMentionSet = new Set(previousMentionUsernames);
    const nextMentionedUsers = mentionResolution.mentionedUsers.filter(
      (mentionedUser) => !previousMentionSet.has(mentionedUser.username),
    );
    await this.notifyMentionedUsers(
      userId,
      comment.postId.toString(),
      nextMentionedUsers,
      nextContent.slice(0, 160),
      new Set<string>(),
    );

    return updated;
  }

  async removeBySuperAdmin(superAdminIdentifier: string, commentId: string) {
    const comment = await this.commentsRepository.findById(commentId);
    if (!comment) {
      throw new NotFoundException('Comment not found');
    }

    await this.commentsRepository.deleteById(commentId, superAdminIdentifier);
    await this.syncCommentsCount(comment.postId.toString());
    await this.feedVersionService.bumpGlobalVersion();
    await this.auditService.logSuperAdminAction(
      superAdminIdentifier,
      'comment_delete',
      'comment',
      commentId,
      { postId: comment.postId.toString() },
    );
    return { success: true, message: 'Comment deleted by superadmin' };
  }

  async findByPost(viewerId: string, postId: string, query: CommentQueryDto) {
    if (!Types.ObjectId.isValid(postId)) {
      throw new BadRequestException('Invalid post id');
    }

    const page = query.page ?? 1;
    const limit = query.limit ?? 20;
    const skip = (page - 1) * limit;
    const postObjectId = new Types.ObjectId(postId);
    const sort = this.resolveCommentSort(query);
    const filter = {
      postId: postObjectId,
      $or: [{ parentCommentId: { $exists: false } }, { parentCommentId: null }],
    };

    const [items, total] = await Promise.all([
      query.sortBy === CommentSortBy.TOP
        ? this.commentsRepository.findManyTop(
            filter,
            skip,
            limit,
            resolveMongoSortDirection(query.sortOrder),
          )
        : this.commentsRepository.findMany(filter, skip, limit, sort),
      this.commentsRepository.count(filter),
    ]);
    const enrichedItems = await this.enrichComments(items, viewerId, {
      includeRepliesPreview: true,
    });

    return buildPaginatedResponse(enrichedItems, {
      page,
      limit,
      total,
      offset: skip,
    });
  }

  async findReplies(viewerId: string, parentCommentId: string, query: CommentQueryDto) {
    if (!Types.ObjectId.isValid(parentCommentId)) {
      throw new BadRequestException('Invalid parent comment id');
    }

    const page = query.page ?? 1;
    const limit = query.limit ?? 20;
    const skip = (page - 1) * limit;
    const parentObjectId = new Types.ObjectId(parentCommentId);
    const sort = this.resolveCommentSort(query);

    const filter = { parentCommentId: parentObjectId };
    const [items, total, parent] = await Promise.all([
      query.sortBy === CommentSortBy.TOP
        ? this.commentsRepository.findManyTop(
            filter,
            skip,
            limit,
            resolveMongoSortDirection(query.sortOrder),
          )
        : this.commentsRepository.findMany(filter, skip, limit, sort),
      this.commentsRepository.count({ parentCommentId: parentObjectId }),
      this.commentsRepository.findByIdWithAuthor(parentCommentId),
    ]);
    const enrichedItems = await this.enrichComments(items, viewerId, {
      replyToUser: this.extractAuthorSummary(parent?.authorId),
    });

    return buildPaginatedResponse(enrichedItems, {
      page,
      limit,
      total,
      offset: skip,
    });
  }

  async findPlatformComments(query: AdminCommentQueryDto) {
    const page = query.page ?? 1;
    const limit = query.limit ?? 20;
    const skip = (page - 1) * limit;
    const filter: Record<string, unknown> = {};

    if (query.postId) {
      filter.postId = new Types.ObjectId(query.postId);
    }
    if (query.authorId) {
      filter.authorId = new Types.ObjectId(query.authorId);
    }
    if (query.q?.trim()) {
      filter.content = { $regex: escapeRegex(query.q.trim()), $options: 'i' };
    }
    if (query.moderationStatus) {
      filter.moderationStatus = query.moderationStatus;
    }

    const sort = { createdAt: resolveMongoSortDirection(query.sortOrder) } as Record<string, 1 | -1>;
    const [items, total] = await Promise.all([
      this.commentsRepository.findManyAdmin(filter, skip, limit, sort),
      this.commentsRepository.countAdmin(filter),
    ]);

    return buildPaginatedResponse(items, {
      page,
      limit,
      total,
      offset: skip,
    });
  }

  async updateModerationStatusBySuperAdmin(
    superAdminIdentifier: string,
    commentId: string,
    dto: { status: ModerationStatus; reason?: string },
  ) {
    const comment = await this.commentsRepository.findById(commentId);
    if (!comment) {
      throw new NotFoundException('Comment not found');
    }

    const updated = await this.commentsRepository.updateModerationStatus(commentId, {
      moderationStatus: dto.status,
      moderationReason: dto.reason?.trim() ?? '',
    });
    if (!updated) {
      throw new NotFoundException('Comment not found');
    }

    await this.feedVersionService.bumpGlobalVersion();
    await this.auditService.logSuperAdminAction(
      superAdminIdentifier,
      'comment_moderation_status_update',
      'comment',
      commentId,
      {
        previousStatus: comment.moderationStatus ?? ModerationStatus.ACTIVE,
        nextStatus: dto.status,
        reason: dto.reason?.trim() ?? '',
      },
    );

    return updated;
  }

  private async syncCommentsCount(postId: string): Promise<void> {
    const totalComments = await this.commentsRepository.countByPost(postId);
    await this.postsRepository.setCommentsCount(postId, totalComments);
  }

  private resolveCommentSort(query: CommentQueryDto): Record<string, 1 | -1> {
    return { isPinned: -1, createdAt: resolveMongoSortDirection(query.sortOrder) };
  }

  private async assertCanComment(userId: string, post: any): Promise<void> {
    if (post.commentsDisabled) {
      throw new ForbiddenException('Comments are disabled for this post');
    }

    const authorId = this.extractEntityId(post.authorId);
    await this.assertNoBlockBetween(userId, authorId);
    if (!post.commentsFollowersOnly || authorId === userId) {
      return;
    }

    const followsAuthor = await this.followsRepository.findOne(userId, authorId);
    if (!followsAuthor) {
      throw new ForbiddenException('Only followers can comment on this post');
    }
  }

  private matchesCommentFilter(content: string, keywords: string[] = []): boolean {
    const normalized = content.toLowerCase();
    return keywords
      .map((keyword) => keyword.trim().toLowerCase())
      .filter(Boolean)
      .some((keyword) => normalized.includes(keyword));
  }

  private async enrichComments(
    comments: CommentDocument[],
    viewerId: string,
    options: {
      includeRepliesPreview?: boolean;
      replyToUser?: CommentAuthorSummary | null;
    } = {},
  ): Promise<InstagramComment[]> {
    if (!comments.length) {
      return [];
    }

    const commentIds = comments.map((comment) => comment.id);
    const [repliesCountByCommentId, likesCountByCommentId, likedCommentIds, previewsByCommentId] =
      await Promise.all([
        this.commentsRepository.countRepliesByParentIds(commentIds),
        this.commentsRepository.countLikesByCommentIds(commentIds),
        this.commentsRepository.findLikedCommentIds(viewerId, commentIds),
        options.includeRepliesPreview
          ? this.commentsRepository.findReplyPreviewsByParentIds(commentIds, 2)
          : Promise.resolve({} as Record<string, CommentDocument[]>),
      ]);

    const likedCommentIdSet = new Set(likedCommentIds);
    const previewCommentIds = Object.values(previewsByCommentId)
      .flat()
      .map((comment) => comment.id);
    const [previewLikesCountByCommentId, previewLikedCommentIds] = await Promise.all([
      this.commentsRepository.countLikesByCommentIds(previewCommentIds),
      this.commentsRepository.findLikedCommentIds(viewerId, previewCommentIds),
    ]);
    const previewLikedCommentIdSet = new Set(previewLikedCommentIds);

    return comments.map((comment) => {
      const commentId = comment.id;
      const repliesPreview = (previewsByCommentId[commentId] ?? []).map((reply) =>
        this.serializeComment(reply, {
          viewerId,
          likesCount: previewLikesCountByCommentId[reply.id] ?? 0,
          likedByMe: previewLikedCommentIdSet.has(reply.id),
          repliesCount: 0,
          repliesPreview: [],
          replyToUser: this.extractAuthorSummary(comment.authorId),
        }),
      );

      return this.serializeComment(comment, {
        viewerId,
        likesCount: likesCountByCommentId[commentId] ?? 0,
        likedByMe: likedCommentIdSet.has(commentId),
        repliesCount: repliesCountByCommentId[commentId] ?? 0,
        repliesPreview,
        replyToUser: options.replyToUser ?? null,
      });
    });
  }

  private serializeComment(
    comment: CommentDocument,
    params: {
      viewerId: string;
      likesCount: number;
      likedByMe: boolean;
      repliesCount: number;
      repliesPreview: InstagramComment[];
      replyToUser: CommentAuthorSummary | null;
    },
  ): InstagramComment {
    const plain: Record<string, unknown> =
      typeof comment.toObject === 'function'
        ? (comment.toObject({ virtuals: true }) as Record<string, unknown>)
        : (comment as unknown as Record<string, unknown>);
    const authorId = this.extractEntityId(plain.authorId ?? comment.authorId);
    const isDeleted = Boolean(plain.isDeleted);

    return {
      ...plain,
      id: plain.id ?? comment.id,
      content: isDeleted ? 'This comment was deleted' : plain.content,
      repliesCount: params.repliesCount,
      repliesPreview: params.repliesPreview,
      likesCount: params.likesCount,
      likedByMe: params.likedByMe,
      canEdit: !isDeleted && authorId === params.viewerId,
      canDelete: !isDeleted && authorId === params.viewerId,
      replyToUser: params.replyToUser,
    };
  }

  private extractAuthorSummary(value: unknown): CommentAuthorSummary | null {
    if (!value || value instanceof Types.ObjectId || typeof value === 'string') {
      return null;
    }

    if (typeof value !== 'object') {
      return null;
    }

    const candidate = value as {
      _id?: unknown;
      id?: unknown;
      name?: string;
      username?: string;
      avatar?: string;
      stageName?: string;
      isVerified?: boolean;
    };
    const id = this.extractEntityId(candidate);
    if (!id) {
      return null;
    }

    return {
      id,
      name: candidate.name,
      username: candidate.username,
      avatar: candidate.avatar,
      stageName: candidate.stageName,
      isVerified: candidate.isVerified,
    };
  }

  private async dispatchCommentNotifications(
    actorId: string,
    postAuthorId: string,
    parentRecipientId: string,
    postId: string,
    previewText: string,
  ): Promise<Set<string>> {
    const recipients = new Set<string>();
    if (postAuthorId && postAuthorId !== actorId) {
      recipients.add(postAuthorId);
    }
    if (parentRecipientId && parentRecipientId !== actorId) {
      recipients.add(parentRecipientId);
    }

    for (const recipientId of recipients) {
      try {
        await this.notificationsService.createCommentNotification(actorId, recipientId, postId, {
          resourceType: 'post',
          previewText,
        });
      } catch (error) {
        this.logger.warn(
          `Comment notification failed for actor=${actorId} recipient=${recipientId}: ${
            error instanceof Error ? error.message : 'unknown error'
          }`,
        );
      }
    }

    return recipients;
  }

  private normalizeMentionUsernames(input: string[] = []): string[] {
    return Array.from(
      new Set(
        input
          .map((username) => username?.trim().replace(/^@+/, '').toLowerCase())
          .filter((username): username is string => !!username),
      ),
    );
  }

  private extractMentions(content: string): string[] {
    const matches = content.match(/@[\p{L}\p{N}_.]+/gu) ?? [];
    return this.normalizeMentionUsernames(matches.map((item) => item.replace('@', '')));
  }

  private async resolveMentionTargets(
    explicitMentionUsernames: string[] | undefined,
    content: string,
    authorId: string,
  ): Promise<{
    mentionUsernames: string[];
    mentionedUsers: Array<{ id: string; username: string }>;
  }> {
    const mergedMentionUsernames = Array.from(
      new Set([
        ...this.extractMentions(content),
        ...this.normalizeMentionUsernames(explicitMentionUsernames ?? []),
      ]),
    );

    if (mergedMentionUsernames.length > 30) {
      throw new BadRequestException('You can mention up to 30 users only');
    }

    if (!mergedMentionUsernames.length) {
      return { mentionUsernames: [], mentionedUsers: [] };
    }

    const users = await this.usersRepository.findByUsernames(mergedMentionUsernames);
    const userByUsername = new Map(
      users.map((user) => [user.username.toLowerCase(), { id: user.id, username: user.username.toLowerCase() }]),
    );

    const mentionedUsers = mergedMentionUsernames
      .map((username) => userByUsername.get(username))
      .filter((user): user is { id: string; username: string } => !!user)
      .filter((user) => user.id !== authorId);

    return {
      mentionUsernames: mentionedUsers.map((user) => user.username),
      mentionedUsers,
    };
  }

  private async notifyMentionedUsers(
    actorId: string,
    postId: string,
    mentionedUsers: Array<{ id: string; username: string }>,
    previewText: string,
    excludedRecipientIds: Set<string>,
  ): Promise<void> {
    if (!mentionedUsers.length) {
      return;
    }

    for (const mentionedUser of mentionedUsers) {
      if (excludedRecipientIds.has(mentionedUser.id)) {
        continue;
      }

      try {
        await this.notificationsService.createMentionNotification(actorId, mentionedUser.id, postId, {
          resourceType: 'comment',
          previewText,
          deepLink: `/posts/${postId}`,
        });
      } catch (error) {
        this.logger.warn(
          `Comment mention notification failed for actor=${actorId} recipient=${mentionedUser.id}: ${
            error instanceof Error ? error.message : 'unknown error'
          }`,
        );
      }
    }
  }

  private async assertNoBlockBetween(userId: string, targetUserId: string): Promise<void> {
    if (!targetUserId || userId === targetUserId) {
      return;
    }
    const block = await this.blocksRepository.findAnyBetween(userId, targetUserId);
    if (block) {
      throw new ForbiddenException('You cannot interact with this user');
    }
  }

  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 '';
  }
}
