import { ForbiddenException, Injectable, Logger, NotFoundException } from '@nestjs/common';
import { Types } from 'mongoose';
import { PaginationQueryDto } from '../../common/dto/pagination-query.dto';
import { buildPaginatedResponse } from '../../common/utils/pagination.util';
import { resolveMongoSortDirection } from '../../common/utils/sort.util';
import { FeedVersionService } from '../../infrastructure/cache/feed-version.service';
import { BlocksRepository } from '../blocks/blocks.repository';
import { NotificationsService } from '../notifications/notifications.service';
import { PostsRepository } from '../posts/posts.repository';
import { PostsService } from '../posts/posts.service';
import { ToggleSaveDto } from './dto/toggle-save.dto';
import { SavesRepository } from './saves.repository';

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

  constructor(
    private readonly savesRepository: SavesRepository,
    private readonly postsRepository: PostsRepository,
    private readonly feedVersionService: FeedVersionService,
    private readonly notificationsService: NotificationsService,
    private readonly blocksRepository: BlocksRepository,
    private readonly postsService: PostsService,
  ) {}

  async toggle(userId: string, dto: ToggleSaveDto): Promise<{ saved: boolean; postId: string }> {
    const existing = await this.savesRepository.findOne(userId, dto.postId);
    return existing ? this.unsave(userId, dto) : this.save(userId, dto);
  }

  async save(userId: string, dto: ToggleSaveDto): Promise<{ saved: boolean; postId: string }> {
    const post = await this.getPostOrThrow(dto.postId);
    await this.assertCanSave(userId, post);

    const existing = await this.savesRepository.findOne(userId, dto.postId);
    if (existing) {
      return { saved: true, postId: dto.postId };
    }

    const created = await this.savesRepository.createIfAbsent(userId, dto.postId);
    if (!created) {
      return { saved: true, postId: dto.postId };
    }
    await this.postsRepository.incrementSavesCount(dto.postId, 1);
    await this.feedVersionService.bumpUserVersion(userId);
    const recipientId = this.extractEntityId(post.authorId);
    if (recipientId && recipientId !== userId) {
      try {
        await this.notificationsService.createSaveNotification(userId, recipientId, dto.postId, {
          resourceType: 'post',
          previewText: (post.content ?? '').slice(0, 140),
        });
      } catch (error) {
        this.logger.warn(
          `Save notification failed for actor=${userId} recipient=${recipientId}: ${
            error instanceof Error ? error.message : 'unknown error'
          }`,
        );
      }
    }
    return { saved: true, postId: dto.postId };
  }

  async unsave(userId: string, dto: ToggleSaveDto): Promise<{ saved: boolean; postId: string }> {
    await this.assertPostExists(dto.postId);

    const existing = await this.savesRepository.findOne(userId, dto.postId);
    if (!existing) {
      return { saved: false, postId: dto.postId };
    }

    const deleted = await this.savesRepository.deleteById(existing.id);
    if (!deleted) {
      return { saved: false, postId: dto.postId };
    }
    await this.postsRepository.incrementSavesCount(dto.postId, -1);
    await this.feedVersionService.bumpUserVersion(userId);
    return { saved: false, postId: dto.postId };
  }

  async getStatus(userId: string, dto: ToggleSaveDto): Promise<{ saved: boolean; postId: string }> {
    const post = await this.postsRepository.findById(dto.postId);
    if (!post || !(await this.postsService.canViewerSeePost(userId, post))) {
      return { saved: false, postId: dto.postId };
    }

    const existing = await this.savesRepository.findOne(userId, dto.postId);
    return { saved: !!existing, postId: dto.postId };
  }

  async getMySavedPosts(userId: string, query: PaginationQueryDto) {
    const page = query.page ?? 1;
    const limit = query.limit ?? 20;
    const skip = (page - 1) * limit;
    const sort = { createdAt: resolveMongoSortDirection(query.sortOrder) } as Record<string, 1 | -1>;

    const [postIds, total] = await Promise.all([
      this.savesRepository.findUserSavedPostIds(userId, skip, limit, sort),
      this.savesRepository.countByUser(userId),
    ]);

    const candidates = await this.postsRepository.findManyByIds(postIds);
    const visibility = await Promise.all(
      candidates.map((post) => this.postsService.canViewerSeePost(userId, post)),
    );
    const items = candidates.filter((_, index) => visibility[index]);

    return buildPaginatedResponse(items, {
      page,
      limit,
      total: Math.max(0, total - (candidates.length - items.length)),
      offset: skip,
    });
  }

  private async assertPostExists(postId: string): Promise<void> {
    await this.getPostOrThrow(postId);
  }

  private async postExists(postId: string): Promise<boolean> {
    const post = await this.postsRepository.findById(postId);
    return !!post;
  }

  private async getPostOrThrow(postId: string) {
    const post = await this.postsRepository.findById(postId);
    if (!post) {
      throw new NotFoundException('Post not found');
    }
    return post;
  }

  private async assertCanSave(userId: string, post: any): Promise<void> {
    if (!(await this.postsService.canViewerSeePost(userId, post))) {
      throw new NotFoundException('Post not found');
    }

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

    const block = await this.blocksRepository.findAnyBetween(userId, authorId);
    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 '';
  }
}
