import { ForbiddenException, Injectable, Logger, NotFoundException } from '@nestjs/common';
import { Types } from 'mongoose';
import { ReactionType } from '../../common/enums/reaction-type.enum';
import { FeedVersionService } from '../../infrastructure/cache/feed-version.service';
import { NotificationsService } from '../notifications/notifications.service';
import { BlocksRepository } from '../blocks/blocks.repository';
import { CommentsRepository } from '../comments/comments.repository';
import { PostsRepository } from '../posts/posts.repository';
import { PostsService } from '../posts/posts.service';
import { LikesRepository } from './likes.repository';
import { ToggleLikeDto } from './dto/toggle-like.dto';

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

  constructor(
    private readonly likesRepository: LikesRepository,
    private readonly postsRepository: PostsRepository,
    private readonly commentsRepository: CommentsRepository,
    private readonly feedVersionService: FeedVersionService,
    private readonly notificationsService: NotificationsService,
    private readonly blocksRepository: BlocksRepository,
    private readonly postsService: PostsService,
  ) {}

  async toggle(userId: string, dto: ToggleLikeDto) {
    const existing = await this.likesRepository.findOne(userId, dto.targetId, dto.targetType);
    return existing ? this.unlike(userId, dto) : this.like(userId, dto);
  }

  async like(userId: string, dto: ToggleLikeDto) {
    await this.assertTargetExists(dto);
    await this.assertCanLike(userId, dto);
    const notificationContext = await this.resolveNotificationContext(dto);
    const reactionType = dto.reactionType ?? ReactionType.LIKE;

    const existing = await this.likesRepository.findOne(userId, dto.targetId, dto.targetType);
    if (existing) {
      if ((existing.reactionType ?? ReactionType.LIKE) !== reactionType) {
        await this.likesRepository.updateReaction(existing.id, reactionType);
      }
      return {
        liked: true,
        reacted: true,
        targetId: dto.targetId,
        targetType: dto.targetType,
        reactionType,
      };
    }

    const created = await this.likesRepository.createIfAbsent(
      userId,
      dto.targetId,
      dto.targetType,
      reactionType,
    );
    if (!created) {
      return { liked: true, reacted: true, targetId: dto.targetId, targetType: dto.targetType, reactionType };
    }
    if (dto.targetType === 'post') {
      await this.postsRepository.incrementLikesCount(dto.targetId, 1);
    }
    await this.feedVersionService.bumpUserVersion(userId);
    if (notificationContext.recipientId && notificationContext.recipientId !== userId) {
      try {
        await this.notificationsService.createLikeNotification(
          userId,
          notificationContext.recipientId,
          dto.targetId,
          {
            resourceType: dto.targetType,
            previewText: notificationContext.previewText,
          },
        );
      } catch (error) {
        this.logger.warn(
          `Like notification failed for actor=${userId} recipient=${notificationContext.recipientId}: ${
            error instanceof Error ? error.message : 'unknown error'
          }`,
        );
      }
    }

    return { liked: true, reacted: true, targetId: dto.targetId, targetType: dto.targetType, reactionType };
  }

  async unlike(userId: string, dto: ToggleLikeDto) {
    await this.assertTargetExists(dto);

    const existing = await this.likesRepository.findOne(userId, dto.targetId, dto.targetType);
    if (!existing) {
      return { liked: false, reacted: false, targetId: dto.targetId, targetType: dto.targetType };
    }

    const deleted = await this.likesRepository.deleteById(existing.id);
    if (!deleted) {
      return { liked: false, reacted: false, targetId: dto.targetId, targetType: dto.targetType };
    }
    if (dto.targetType === 'post') {
      await this.postsRepository.incrementLikesCount(dto.targetId, -1);
    }
    await this.feedVersionService.bumpUserVersion(userId);

    return { liked: false, reacted: false, targetId: dto.targetId, targetType: dto.targetType };
  }

  async getStatus(userId: string, dto: ToggleLikeDto) {
    if (!(await this.targetVisibleTo(userId, dto))) {
      return { liked: false, reacted: false, targetId: dto.targetId, targetType: dto.targetType };
    }

    const [existing, reactionSummary] = await Promise.all([
      this.likesRepository.findOne(userId, dto.targetId, dto.targetType),
      this.likesRepository.getReactionSummary(dto.targetId, dto.targetType),
    ]);
    return {
      liked: !!existing,
      reacted: !!existing,
      targetId: dto.targetId,
      targetType: dto.targetType,
      reactionType: existing?.reactionType ?? null,
      reactionSummary,
    };
  }

  private async assertTargetExists(dto: ToggleLikeDto): Promise<void> {
    const targetExists = await this.targetExists(dto);
    if (!targetExists) {
      throw new NotFoundException(dto.targetType === 'post' ? 'Post not found' : 'Comment not found');
    }
  }

  private async assertCanLike(userId: string, dto: ToggleLikeDto): Promise<void> {
    if (dto.targetType === 'post') {
      const post = await this.postsRepository.findById(dto.targetId);
      if (!post || !(await this.postsService.canViewerSeePost(userId, post))) {
        throw new NotFoundException('Post not found');
      }
    } else {
      const comment = await this.commentsRepository.findById(dto.targetId);
      const post = comment ? await this.postsRepository.findById(comment.postId.toString()) : null;
      if (!post || !(await this.postsService.canViewerSeePost(userId, post))) {
        throw new NotFoundException('Comment not found');
      }
    }

    const ownerId = await this.resolveTargetOwnerId(dto);
    if (!ownerId || ownerId === userId) {
      return;
    }

    const block = await this.blocksRepository.findAnyBetween(userId, ownerId);
    if (block) {
      throw new ForbiddenException('You cannot interact with this user');
    }
  }

  private async targetExists(dto: ToggleLikeDto): Promise<boolean> {
    if (dto.targetType === 'post') {
      const post = await this.postsRepository.findById(dto.targetId);
      return !!post;
    }

    const comment = await this.commentsRepository.findById(dto.targetId);
    return !!comment;
  }

  private async resolveNotificationContext(
    dto: ToggleLikeDto,
  ): Promise<{ recipientId: string; previewText: string }> {
    if (dto.targetType === 'post') {
      const post = await this.postsRepository.findById(dto.targetId);
      return {
        recipientId: this.extractEntityId(post?.authorId),
        previewText: (post?.content ?? '').slice(0, 140),
      };
    }

    const comment = await this.commentsRepository.findById(dto.targetId);
    return {
      recipientId: comment?.authorId?.toString?.() ?? '',
      previewText: (comment?.content ?? '').slice(0, 140),
    };
  }

  private async targetVisibleTo(userId: string, dto: ToggleLikeDto): Promise<boolean> {
    if (dto.targetType === 'post') {
      const post = await this.postsRepository.findById(dto.targetId);
      return !!post && this.postsService.canViewerSeePost(userId, post);
    }
    const comment = await this.commentsRepository.findById(dto.targetId);
    if (!comment) return false;
    const post = await this.postsRepository.findById(comment.postId.toString());
    return !!post && this.postsService.canViewerSeePost(userId, post);
  }

  private async resolveTargetOwnerId(dto: ToggleLikeDto): Promise<string> {
    if (dto.targetType === 'post') {
      const post = await this.postsRepository.findById(dto.targetId);
      return this.extractEntityId(post?.authorId);
    }

    const comment = await this.commentsRepository.findById(dto.targetId);
    return comment?.authorId?.toString?.() ?? '';
  }

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