import {
  BadRequestException,
  ForbiddenException,
  Injectable,
  Logger,
  NotFoundException,
} from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { Connection, Types } from 'mongoose';
import { InjectConnection } from '@nestjs/mongoose';
import { ModerationStatus } from '../../common/enums/moderation-status.enum';
import { PostType } from '../../common/enums/post-type.enum';
import { PostVisibility } from '../../common/enums/post-visibility.enum';
import { ProcessingStatus } from '../../common/enums/processing-status.enum';
import { buildPaginatedResponse } from '../../common/utils/pagination.util';
import { assertAllowedMediaFile, MEDIA_MAX_SIZE_BYTES } from '../../common/media/allowed-media';
import { resolveManagedFileUrl } from '../../common/utils/public-url.util';
import { buildPostShareUrl, resolveShareBaseUrl } from '../../common/utils/share-url.util';
import { resolveMongoSortDirection } from '../../common/utils/sort.util';
import { escapeRegex } from '../../common/utils/regex.util';
import {
  buildWaveformPeakSet,
  generateWaveformPeakSetFromBuffer,
  generateWaveformPeakSetFromSeed,
  WaveformPeakSet,
} from '../../common/utils/waveform.util';
import { FeedVersionService } from '../../infrastructure/cache/feed-version.service';
import { AppCacheService } from '../../infrastructure/cache/app-cache.service';
import { ContentModerationService } from '../moderation/content-moderation.service';
import {
  ImageProcessingService,
  UploadedImageFile,
} from '../../infrastructure/storage/image-processing.service';
import { ManagedStorageService } from '../../infrastructure/storage/managed-storage.service';
import {
  UploadedVideoFile,
  VideoProcessingService,
} from '../../infrastructure/storage/video-processing.service';
import { MediaProbeService } from '../../infrastructure/storage/media-probe.service';
import { NotificationsService } from '../notifications/notifications.service';
import { AuditService } from '../audit/audit.service';
import { BlocksRepository } from '../blocks/blocks.repository';
import { ChatService } from '../chat/chat.service';
import { FollowsRepository } from '../follows/follows.repository';
import { UsersRepository } from '../users/users.repository';
import { AdminPostQueryDto } from './dto/admin-post-query.dto';
import { CreateReelDto } from './dto/create-reel.dto';
import { CreatePostDto } from './dto/create-post.dto';
import { CreateRepostDto } from './dto/create-repost.dto';
import { PostQueryDto } from './dto/post-query.dto';
import { ReelQueryDto } from './dto/reel-query.dto';
import {
  PostShareChannel,
  PostShareTarget,
  SharePostDto,
  SharePostExternalDto,
  SharePostToFriendDto,
  SharePostToProfileDto,
} from './dto/share-post.dto';
import { UpdateCommentSettingsDto } from './dto/update-comment-settings.dto';
import { UpdatePostDto } from './dto/update-post.dto';
import { PostDocument, PostImageItem, PostMediaVariantSet } from './schemas/post.schema';
import { PostsRepository } from './posts.repository';

type PostMediaMetadataInput = Pick<
  CreatePostDto,
  'durationSeconds' | 'thumbnailUrl' | 'style' | 'maqam' | 'rhythmSignature' | 'waveformPeaks'
>;

type NormalizedPostMediaMetadata = {
  durationSeconds: number | null;
  thumbnailUrl: string;
  style: string;
  maqam: string;
  rhythmSignature: string;
  waveformPeaks: number[];
  waveformPeaksPreview: number[];
  waveformPeaksDetailed: number[];
};

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

type SavedVideoUpload = {
  videoUrl: string;
  hlsUrl: string;
  thumbnailUrl: string;
  thumbnailVariants: PostMediaVariantSet | null;
  durationSeconds: number | null;
};

type SavedImageUpload = {
  primaryUrl: string;
  variants: PostMediaVariantSet;
};

type ProfileShareRecord = {
  _id: Types.ObjectId;
  userId: Types.ObjectId;
  postId: Types.ObjectId;
  originalAuthorId?: Types.ObjectId;
  sharedFrom?: { type?: string; id?: string; name?: string } | null;
  createdAt?: Date;
  updatedAt?: Date;
  isDeleted?: boolean;
  deletedAt?: Date | null;
};

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

  constructor(
    @InjectConnection() private readonly connection: Connection,
    private readonly configService: ConfigService,
    private readonly postsRepository: PostsRepository,
    private readonly usersRepository: UsersRepository,
    private readonly storageService: ManagedStorageService,
    private readonly imageProcessingService: ImageProcessingService,
    private readonly videoProcessingService: VideoProcessingService,
    private readonly mediaProbeService: MediaProbeService,
    private readonly feedVersionService: FeedVersionService,
    private readonly notificationsService: NotificationsService,
    private readonly auditService: AuditService,
    private readonly chatService: ChatService,
    private readonly followsRepository: FollowsRepository,
    private readonly blocksRepository: BlocksRepository,
    private readonly appCacheService: AppCacheService,
    private readonly contentModerationService: ContentModerationService,
  ) {}

  async create(
    userId: string,
    dto: CreatePostDto,
    imageFiles: Array<{
      mimetype?: string;
      size: number;
      buffer: Buffer;
      originalname?: string;
    }> = [],
    videoFile?: { mimetype?: string; size: number; buffer: Buffer; originalname?: string },
    audioFile?: { mimetype?: string; size: number; buffer: Buffer; originalname?: string },
    coverImageFile?: { mimetype?: string; size: number; buffer: Buffer; originalname?: string },
  ): Promise<PostDocument> {
    const inputImageUrls = dto.imageUrls ?? [];
    if (inputImageUrls.length > 10 || imageFiles.length > 10) {
      throw new BadRequestException('Post can contain up to 10 images');
    }
    if (imageFiles.length && inputImageUrls.length) {
      throw new BadRequestException('Provide either imageFiles or imageUrls, not both');
    }
    if (videoFile && audioFile) {
      throw new BadRequestException('Post can contain either images, video, or audio');
    }
    if (videoFile && dto.videoUrl) {
      throw new BadRequestException('Provide either videoFile or videoUrl, not both');
    }
    if (audioFile && dto.audioUrl) {
      throw new BadRequestException('Provide either audioFile or audioUrl, not both');
    }
    if (coverImageFile && dto.thumbnailUrl) {
      throw new BadRequestException('Provide either coverImageFile or thumbnailUrl, not both');
    }
    if (coverImageFile && !(videoFile || audioFile || dto.videoUrl || dto.audioUrl)) {
      throw new BadRequestException('coverImageFile is allowed only with video or audio posts');
    }
    if ((videoFile || dto.videoUrl) && (imageFiles.length || inputImageUrls.length)) {
      throw new BadRequestException('Post can contain either images or video, not both');
    }
    if ((audioFile || dto.audioUrl) && (imageFiles.length || inputImageUrls.length)) {
      throw new BadRequestException('Post can contain either images or audio, not both');
    }

    const savedImageUploads = await this.saveImageFiles(imageFiles);
    const uploadedImageUrls = savedImageUploads.map((item) => item.primaryUrl);
    const uploadedImageVariants = savedImageUploads.map((item) => item.variants);
    const savedVideoUpload = videoFile ? await this.saveVideoUpload(videoFile) : null;
    const savedCoverImageUpload = coverImageFile
      ? await this.saveResponsiveImageAsset('thumbnails', coverImageFile)
      : null;
    const uploadedVideoUrl = savedVideoUpload?.videoUrl ?? '';
    const uploadedHlsUrl = savedVideoUpload?.hlsUrl ?? '';
    const generatedThumbnailUrl = savedVideoUpload?.thumbnailUrl ?? '';
    const generatedThumbnailVariants = savedVideoUpload?.thumbnailVariants ?? null;
    const uploadedThumbnailUrl = savedCoverImageUpload?.primaryUrl ?? generatedThumbnailUrl;
    const uploadedThumbnailVariants = savedCoverImageUpload?.variants ?? generatedThumbnailVariants;
    const uploadedAudioDurationSeconds = audioFile
      ? await this.mediaProbeService.extractDurationSecondsFromBuffer(audioFile.buffer, audioFile)
      : null;
    const uploadedAudioUrl = audioFile ? await this.saveMediaFile('audio', audioFile) : '';
    const finalImageUrls = uploadedImageUrls.length ? uploadedImageUrls : inputImageUrls;
    const finalImageVariants = uploadedImageVariants.length ? uploadedImageVariants : [];
    const finalVideoUrl = uploadedVideoUrl || dto.videoUrl || '';
    const finalAudioUrl = uploadedAudioUrl || dto.audioUrl || '';
    const finalContent = dto.content?.trim() ?? '';
    const finalContentTop = dto.contentTop?.trim() ?? '';
    const finalContentBottom = dto.contentBottom?.trim() ?? '';
    const combinedText = this.combinePostText(finalContent, finalContentTop, finalContentBottom);
    const taggedUserIds = await this.normalizeTaggedUserIds(dto.taggedUserIds, userId);
    const collaboratorIds = await this.normalizeUserIdList(
      dto.collaboratorIds,
      userId,
      5,
      'collaborator',
    );
    const imageItems = this.buildImageItems(finalImageUrls, dto.imageCaptions, dto.imageAltTexts);
    const mentionResolution = await this.resolveMentionTargets(
      dto.mentionUsernames,
      dto.mentionedUserIds,
      combinedText,
      userId,
    );
    const { location, latitude, longitude } = this.normalizeLocation(dto);
    if (!combinedText && !finalImageUrls.length && !finalVideoUrl && !finalAudioUrl) {
      throw new BadRequestException('Post must contain caption or media');
    }

    const postType = this.resolvePostType(finalImageUrls, finalVideoUrl, finalAudioUrl);
    const hashtags = this.extractHashtags(combinedText);
    const moderation = this.contentModerationService.analyze(combinedText);
    const mediaMetadata = this.normalizeMediaMetadata(dto, postType, undefined, {
      audioSourceBuffer: audioFile?.buffer,
      extractedDurationSeconds: savedVideoUpload?.durationSeconds ?? uploadedAudioDurationSeconds,
      waveformSeed: finalAudioUrl || finalContent || `${userId}:${Date.now()}`,
      thumbnailUrl: uploadedThumbnailUrl,
    });

    let post: PostDocument;
    try {
      post = await this.postsRepository.create(userId, {
        content: finalContent,
        contentTop: finalContentTop,
        contentBottom: finalContentBottom,
        imageUrls: finalImageUrls,
        imageItems,
        imageVariants: finalImageVariants,
        videoUrl: finalVideoUrl,
        hlsUrl: uploadedHlsUrl,
        audioUrl: finalAudioUrl,
        thumbnailVariants: uploadedThumbnailVariants,
        taggedUserIds,
        collaboratorIds,
        mentionUsernames: mentionResolution.mentionUsernames,
        mentionedUserIds: mentionResolution.mentionedUserIds,
        location,
        latitude,
        longitude,
        postType,
        processingStatus: ProcessingStatus.READY,
        moderationStatus: moderation.flagged ? ModerationStatus.FLAGGED : ModerationStatus.ACTIVE,
        visibility: dto.visibility ?? PostVisibility.PUBLIC,
        commentsDisabled: dto.commentsDisabled ?? false,
        commentsFollowersOnly: dto.commentsFollowersOnly ?? false,
        hashtags,
        ...mediaMetadata,
      });
    } catch (error) {
      await Promise.all([
        ...savedImageUploads.map((item) => this.deleteSavedImageAsset(item)),
        uploadedVideoUrl ? this.deleteManagedPostMedia(uploadedVideoUrl) : Promise.resolve(),
        uploadedHlsUrl
          ? this.storageService.deleteContainingDirectory(uploadedHlsUrl)
          : Promise.resolve(),
        uploadedThumbnailUrl
          ? this.deleteThumbnailAsset(uploadedThumbnailUrl, uploadedThumbnailVariants)
          : Promise.resolve(),
        generatedThumbnailUrl && generatedThumbnailUrl !== uploadedThumbnailUrl
          ? this.deleteThumbnailAsset(generatedThumbnailUrl, generatedThumbnailVariants)
          : Promise.resolve(),
        uploadedAudioUrl ? this.deleteManagedPostMedia(uploadedAudioUrl) : Promise.resolve(),
      ]);
      throw error;
    }

    if (generatedThumbnailUrl && generatedThumbnailUrl !== uploadedThumbnailUrl) {
      await this.deleteThumbnailAsset(generatedThumbnailUrl, generatedThumbnailVariants);
    }

    await this.usersRepository.incrementPostsCount(userId, 1);
    await this.feedVersionService.bumpGlobalVersion();
    await this.notifyMentionedUsers(
      userId,
      post.id,
      mentionResolution.mentionedUsers,
      combinedText,
    );
    return (await this.postsRepository.findById(post.id)) ?? post;
  }

  async update(
    userId: string,
    postId: string,
    dto: UpdatePostDto,
    imageFiles: Array<{
      mimetype?: string;
      size: number;
      buffer: Buffer;
      originalname?: string;
    }> = [],
    videoFile?: { mimetype?: string; size: number; buffer: Buffer; originalname?: string },
    audioFile?: { mimetype?: string; size: number; buffer: Buffer; originalname?: string },
    coverImageFile?: { mimetype?: string; size: number; buffer: Buffer; originalname?: string },
  ): Promise<PostDocument> {
    const post = await this.postsRepository.findById(postId);
    if (!post) {
      throw new NotFoundException('Post not found');
    }

    if (this.extractEntityId(post.authorId) !== userId) {
      throw new ForbiddenException('You can only update your own posts');
    }

    const inputImageUrls = dto.imageUrls ?? [];
    if (inputImageUrls.length > 10 || imageFiles.length > 10) {
      throw new BadRequestException('Post can contain up to 10 images');
    }
    if (imageFiles.length && inputImageUrls.length) {
      throw new BadRequestException('Provide either imageFiles or imageUrls, not both');
    }

    if (videoFile && audioFile) {
      throw new BadRequestException('Post can contain either images, video, or audio');
    }
    if (videoFile && dto.videoUrl) {
      throw new BadRequestException('Provide either videoFile or videoUrl, not both');
    }
    if (audioFile && dto.audioUrl) {
      throw new BadRequestException('Provide either audioFile or audioUrl, not both');
    }
    if (coverImageFile && dto.thumbnailUrl) {
      throw new BadRequestException('Provide either coverImageFile or thumbnailUrl, not both');
    }
    if (coverImageFile && (imageFiles.length || inputImageUrls.length)) {
      throw new BadRequestException('coverImageFile is allowed only with video or audio posts');
    }
    if (
      coverImageFile &&
      !(videoFile || audioFile || dto.videoUrl || dto.audioUrl || post.videoUrl || post.audioUrl)
    ) {
      throw new BadRequestException('coverImageFile is allowed only with video or audio posts');
    }
    if ((videoFile || dto.videoUrl) && (imageFiles.length || inputImageUrls.length)) {
      throw new BadRequestException('Post can contain either images or video, not both');
    }
    if ((audioFile || dto.audioUrl) && (imageFiles.length || inputImageUrls.length)) {
      throw new BadRequestException('Post can contain either images or audio, not both');
    }

    const savedImageUploads = await this.saveImageFiles(imageFiles);
    const uploadedImageUrls = savedImageUploads.map((item) => item.primaryUrl);
    const uploadedImageVariants = savedImageUploads.map((item) => item.variants);
    const savedVideoUpload = videoFile ? await this.saveVideoUpload(videoFile) : null;
    const savedCoverImageUpload = coverImageFile
      ? await this.saveResponsiveImageAsset('thumbnails', coverImageFile)
      : null;
    const uploadedVideoUrl = savedVideoUpload?.videoUrl ?? '';
    const uploadedHlsUrl = savedVideoUpload?.hlsUrl ?? '';
    const generatedThumbnailUrl = savedVideoUpload?.thumbnailUrl ?? '';
    const generatedThumbnailVariants = savedVideoUpload?.thumbnailVariants ?? null;
    const uploadedThumbnailUrl = savedCoverImageUpload?.primaryUrl ?? generatedThumbnailUrl;
    const uploadedThumbnailVariants = savedCoverImageUpload?.variants ?? generatedThumbnailVariants;
    const uploadedAudioDurationSeconds = audioFile
      ? await this.mediaProbeService.extractDurationSecondsFromBuffer(audioFile.buffer, audioFile)
      : null;
    const uploadedAudioUrl = audioFile ? await this.saveMediaFile('audio', audioFile) : '';
    const hasImageUpdate = typeof dto.imageUrls !== 'undefined' || imageFiles.length > 0;
    const existingImageVariants = Array.isArray((post as any).imageVariants)
      ? (((post as any).imageVariants ?? []) as PostMediaVariantSet[])
      : [];
    const existingThumbnailVariants =
      ((post as any).thumbnailVariants as PostMediaVariantSet | null | undefined) ?? null;

    const hasVideoUpdate = typeof dto.videoUrl !== 'undefined' || !!videoFile;
    const hasAudioUpdate = typeof dto.audioUrl !== 'undefined' || !!audioFile;
    const nextImageUrls = hasImageUpdate
      ? imageFiles.length
        ? uploadedImageUrls
        : inputImageUrls
      : (post.imageUrls ?? []);
    const nextImageVariants = hasImageUpdate
      ? imageFiles.length
        ? uploadedImageVariants
        : []
      : existingImageVariants;

    const nextVideoUrl = hasVideoUpdate
      ? videoFile
        ? uploadedVideoUrl
        : (dto.videoUrl ?? '')
      : (post.videoUrl ?? '');
    const nextHlsUrl = hasVideoUpdate ? (videoFile ? uploadedHlsUrl : '') : (post.hlsUrl ?? '');
    const nextAudioUrl = hasAudioUpdate
      ? audioFile
        ? uploadedAudioUrl
        : (dto.audioUrl ?? '')
      : (post.audioUrl ?? '');
    const nextThumbnailVariants =
      coverImageFile || videoFile
        ? uploadedThumbnailVariants
        : typeof dto.thumbnailUrl === 'string'
          ? null
          : existingThumbnailVariants;
    const nextPostType = this.resolvePostType(nextImageUrls, nextVideoUrl, nextAudioUrl);
    const nextContent = typeof dto.content === 'string' ? dto.content.trim() : (post.content ?? '');
    const nextContentTop =
      typeof dto.contentTop === 'string' ? dto.contentTop.trim() : ((post as any).contentTop ?? '');
    const nextContentBottom =
      typeof dto.contentBottom === 'string' ? dto.contentBottom.trim() : ((post as any).contentBottom ?? '');
    const combinedText = this.combinePostText(nextContent, nextContentTop, nextContentBottom);
    const nextTaggedUserIds =
      typeof dto.taggedUserIds !== 'undefined'
        ? await this.normalizeTaggedUserIds(dto.taggedUserIds, userId)
        : (post.taggedUserIds ?? []).map(
            (id: Types.ObjectId | string) => new Types.ObjectId(id.toString()),
          );
    const nextCollaboratorIds =
      typeof dto.collaboratorIds !== 'undefined'
        ? await this.normalizeUserIdList(dto.collaboratorIds, userId, 5, 'collaborator')
        : (post.collaboratorIds ?? []).map(
            (id: Types.ObjectId | string) => new Types.ObjectId(id.toString()),
          );
    const nextImageItems = this.buildImageItems(
      nextImageUrls,
      dto.imageCaptions,
      dto.imageAltTexts,
      hasImageUpdate ? [] : (post.imageItems ?? []),
    );
    const previousMentionUsernames = this.normalizeMentionUsernames(post.mentionUsernames ?? []);
    const previousMentionedUserIds = (post.mentionedUserIds ?? []).map(
      (id: Types.ObjectId | string | { _id?: unknown; id?: unknown }) => this.extractEntityId(id),
    ).filter(Boolean);
    const shouldRecomputeMentions =
      typeof dto.content === 'string' ||
      typeof dto.contentTop === 'string' ||
      typeof dto.contentBottom === 'string' ||
      typeof dto.mentionUsernames !== 'undefined' ||
      typeof dto.mentionedUserIds !== 'undefined';
    const mentionResolution = shouldRecomputeMentions
      ? await this.resolveMentionTargets(dto.mentionUsernames, dto.mentionedUserIds, combinedText, userId)
      : {
          mentionUsernames: previousMentionUsernames,
          mentionedUserIds: previousMentionedUserIds.map((id) => new Types.ObjectId(id)),
          mentionedUsers: [] as MentionTarget[],
        };
    const {
      location: nextLocation,
      latitude: nextLatitude,
      longitude: nextLongitude,
    } = this.normalizeLocation(dto, {
      location: post.location ?? '',
      latitude: post.latitude ?? null,
      longitude: post.longitude ?? null,
    });
    if (!combinedText && !nextImageUrls.length && !nextVideoUrl && !nextAudioUrl) {
      throw new BadRequestException('Post must contain caption or media');
    }
    const mediaMetadata = this.normalizeMediaMetadata(
      dto,
      nextPostType,
      {
        durationSeconds: post.durationSeconds ?? null,
        thumbnailUrl: uploadedThumbnailUrl || (post.thumbnailUrl ?? ''),
        style: post.style ?? '',
        maqam: post.maqam ?? '',
        rhythmSignature: post.rhythmSignature ?? '',
        waveformPeaks: post.waveformPeaks ?? [],
        waveformPeaksPreview: post.waveformPeaksPreview ?? [],
        waveformPeaksDetailed: post.waveformPeaksDetailed ?? [],
      },
      {
        audioSourceBuffer: audioFile?.buffer,
        extractedDurationSeconds: savedVideoUpload?.durationSeconds ?? uploadedAudioDurationSeconds,
        waveformSeed: nextAudioUrl || combinedText || post.id,
        thumbnailUrl: uploadedThumbnailUrl,
      },
    );

    const payload: Record<string, unknown> = {
      ...dto,
      content: nextContent,
      contentTop: nextContentTop,
      contentBottom: nextContentBottom,
      imageUrls: nextImageUrls,
      imageItems: nextImageItems,
      imageVariants: nextImageVariants,
      hlsUrl: nextHlsUrl,
      thumbnailVariants: nextThumbnailVariants,
      taggedUserIds: nextTaggedUserIds,
      collaboratorIds: nextCollaboratorIds,
      mentionUsernames: mentionResolution.mentionUsernames,
      mentionedUserIds: mentionResolution.mentionedUserIds,
      location: nextLocation,
      latitude: nextLatitude,
      longitude: nextLongitude,
      postType: nextPostType,
      processingStatus: ProcessingStatus.READY,
      ...mediaMetadata,
    };

    if (
      typeof dto.content === 'string' ||
      typeof dto.contentTop === 'string' ||
      typeof dto.contentBottom === 'string'
    ) {
      payload.hashtags = this.extractHashtags(combinedText);
    }
    if (hasImageUpdate) {
      payload.hashtags = this.extractHashtags(combinedText);
    }

    if (hasVideoUpdate && !hasAudioUpdate) {
      payload.audioUrl = '';
    }
    if (hasAudioUpdate && !hasVideoUpdate) {
      payload.videoUrl = '';
      payload.hlsUrl = '';
    }
    if (hasImageUpdate) {
      payload.videoUrl = '';
      payload.audioUrl = '';
      payload.hlsUrl = '';
      payload.thumbnailVariants = null;
    }

    if (videoFile) {
      payload.videoUrl = uploadedVideoUrl;
      payload.hlsUrl = uploadedHlsUrl;
      payload.thumbnailVariants = uploadedThumbnailVariants;
      payload.imageUrls = [];
      payload.imageVariants = [];
      payload.audioUrl = '';
    }
    if (audioFile) {
      payload.audioUrl = uploadedAudioUrl;
      payload.imageUrls = [];
      payload.imageVariants = [];
      payload.videoUrl = '';
      payload.hlsUrl = '';
    }
    if (nextPostType !== PostType.AUDIO && nextPostType !== PostType.VIDEO) {
      payload.thumbnailVariants = null;
    }

    let updated: PostDocument | null;
    try {
      updated = await this.postsRepository.updateById(postId, payload as any);
    } catch (error) {
      await Promise.all([
        ...savedImageUploads.map((item) => this.deleteSavedImageAsset(item)),
        uploadedVideoUrl ? this.deleteManagedPostMedia(uploadedVideoUrl) : Promise.resolve(),
        uploadedHlsUrl
          ? this.storageService.deleteContainingDirectory(uploadedHlsUrl)
          : Promise.resolve(),
        uploadedThumbnailUrl
          ? this.deleteThumbnailAsset(uploadedThumbnailUrl, uploadedThumbnailVariants)
          : Promise.resolve(),
        generatedThumbnailUrl && generatedThumbnailUrl !== uploadedThumbnailUrl
          ? this.deleteThumbnailAsset(generatedThumbnailUrl, generatedThumbnailVariants)
          : Promise.resolve(),
        uploadedAudioUrl ? this.deleteManagedPostMedia(uploadedAudioUrl) : Promise.resolve(),
      ]);
      throw error;
    }
    if (!updated) {
      await Promise.all([
        ...savedImageUploads.map((item) => this.deleteSavedImageAsset(item)),
        uploadedVideoUrl ? this.deleteManagedPostMedia(uploadedVideoUrl) : Promise.resolve(),
        uploadedHlsUrl
          ? this.storageService.deleteContainingDirectory(uploadedHlsUrl)
          : Promise.resolve(),
        uploadedThumbnailUrl
          ? this.deleteThumbnailAsset(uploadedThumbnailUrl, uploadedThumbnailVariants)
          : Promise.resolve(),
        generatedThumbnailUrl && generatedThumbnailUrl !== uploadedThumbnailUrl
          ? this.deleteThumbnailAsset(generatedThumbnailUrl, generatedThumbnailVariants)
          : Promise.resolve(),
        uploadedAudioUrl ? this.deleteManagedPostMedia(uploadedAudioUrl) : Promise.resolve(),
      ]);
      throw new NotFoundException('Post not found');
    }

    if (hasVideoUpdate && (post.videoUrl ?? '') !== (updated.videoUrl ?? '')) {
      await this.deleteManagedPostMedia(post.videoUrl ?? '');
    }
    if ((post.hlsUrl ?? '') !== (updated.hlsUrl ?? '')) {
      await this.storageService.deleteContainingDirectory(post.hlsUrl ?? '');
    }
    if ((post.thumbnailUrl ?? '') !== (updated.thumbnailUrl ?? '')) {
      await this.deleteThumbnailAsset(post.thumbnailUrl ?? '', existingThumbnailVariants);
    }
    if (generatedThumbnailUrl && generatedThumbnailUrl !== uploadedThumbnailUrl) {
      await this.deleteThumbnailAsset(generatedThumbnailUrl, generatedThumbnailVariants);
    }
    if (hasAudioUpdate && (post.audioUrl ?? '') !== (updated.audioUrl ?? '')) {
      await this.deleteManagedPostMedia(post.audioUrl ?? '');
    }
    if (hasImageUpdate) {
      const nextImageSet = new Set(updated.imageUrls ?? []);
      const existingImageAssets = this.buildSavedImageAssets(
        post.imageUrls ?? [],
        existingImageVariants,
      );
      await Promise.all(
        existingImageAssets
          .filter((asset) => !nextImageSet.has(asset.primaryUrl))
          .map((asset) => this.deleteSavedImageAsset(asset)),
      );
    }

    await this.feedVersionService.bumpGlobalVersion();
    if (shouldRecomputeMentions) {
      const previousMentionSet = new Set(previousMentionUsernames);
      const nextMentionedUsers = mentionResolution.mentionedUsers.filter(
        (mentionedUser) => !previousMentionSet.has(mentionedUser.username),
      );
      await this.notifyMentionedUsers(userId, postId, nextMentionedUsers, combinedText);
    }
    return updated;
  }

  async remove(userId: string, postId: string): Promise<void> {
    const post = await this.postsRepository.findById(postId);
    if (!post) {
      throw new NotFoundException('Post not found');
    }

    if (this.extractEntityId(post.authorId) !== userId) {
      throw new ForbiddenException('You can only delete your own posts');
    }

    await this.postsRepository.deleteById(postId, userId);
    await this.usersRepository.incrementPostsCount(userId, -1);
    await this.feedVersionService.bumpGlobalVersion();
    const existingImageVariants = Array.isArray((post as any).imageVariants)
      ? (((post as any).imageVariants ?? []) as PostMediaVariantSet[])
      : [];
    const existingThumbnailVariants =
      ((post as any).thumbnailVariants as PostMediaVariantSet | null | undefined) ?? null;
    const cleanupResults = await Promise.allSettled([
      ...this.buildSavedImageAssets(post.imageUrls ?? [], existingImageVariants).map((asset) =>
        this.deleteSavedImageAsset(asset),
      ),
      this.deleteManagedPostMedia(post.videoUrl ?? ''),
      this.storageService.deleteContainingDirectory(post.hlsUrl ?? ''),
      this.deleteManagedPostMedia(post.audioUrl ?? ''),
      this.deleteThumbnailAsset(post.thumbnailUrl ?? '', existingThumbnailVariants),
    ]);
    const failedCleanupCount = cleanupResults.filter((result) => result.status === 'rejected').length;
    if (failedCleanupCount) {
      this.logger.warn(`Post ${postId} deleted but ${failedCleanupCount} media cleanup operation(s) failed`);
    }
  }

  async findById(postId: string, viewerUserId?: string): Promise<Record<string, unknown>> {
    const post = await this.postsRepository.findById(postId);
    if (!post || !(await this.canViewerSeePost(viewerUserId, post))) {
      throw new NotFoundException('Post not found');
    }
    const sharedPostIds = viewerUserId
      ? await this.findSharedPostIdSet(viewerUserId, [post.id])
      : new Set<string>();
    return this.formatPostItemForViewer(post, sharedPostIds, viewerUserId);
  }

  async findUserPosts(userId: string, query: PostQueryDto, viewerUserId?: string) {
    if (!Types.ObjectId.isValid(userId)) {
      throw new BadRequestException('Invalid user id');
    }

    const page = query.page ?? 1;
    const limit = query.limit ?? 20;
    const skip = (page - 1) * limit;

    const profileOwner = await this.usersRepository.findById(userId);
    if (!profileOwner || profileOwner.isDisabled) {
      throw new NotFoundException('User not found');
    }
    if (
      viewerUserId !== userId &&
      profileOwner.isPrivate &&
      (!viewerUserId || !(await this.isFollowing(viewerUserId, userId)))
    ) {
      return buildPaginatedResponse([], { page, limit, total: 0, offset: skip });
    }

    const filter: Record<string, unknown> = {
      authorId: new Types.ObjectId(userId),
      isArchived: { $ne: true },
    };
    const archivedOnly = query.visibility === 'archived';
    if (archivedOnly) {
      if (!viewerUserId || viewerUserId !== userId) {
        return buildPaginatedResponse([], {
          page,
          limit,
          total: 0,
          offset: skip,
        });
      }
      filter.isArchived = true;
    }
    if (viewerUserId && viewerUserId !== userId) {
      const isBlocked = await this.hasBlockBetween(viewerUserId, userId);
      if (isBlocked) {
        return buildPaginatedResponse([], {
          page,
          limit,
          total: 0,
          offset: skip,
        });
      }
      const isFollowing = await this.isFollowing(viewerUserId, userId);
      filter.visibility = isFollowing
        ? { $in: [PostVisibility.PUBLIC, PostVisibility.FOLLOWERS] }
        : PostVisibility.PUBLIC;
    }
    if (query.visibility && !archivedOnly) {
      const isOwner = !!viewerUserId && viewerUserId === userId;
      if (isOwner || query.visibility === PostVisibility.PUBLIC) {
        filter.visibility = query.visibility;
      } else if (query.visibility === PostVisibility.FOLLOWERS) {
        const allowed = filter.visibility as { $in?: PostVisibility[] } | PostVisibility;
        if (typeof allowed === 'object' && allowed.$in?.includes(PostVisibility.FOLLOWERS)) {
          filter.visibility = PostVisibility.FOLLOWERS;
        } else {
          return buildPaginatedResponse([], { page, limit, total: 0, offset: skip });
        }
      } else {
        return buildPaginatedResponse([], { page, limit, total: 0, offset: skip });
      }
    }
    const postTypeFilter = this.resolvePostTypeFilter(query.mediaType ?? query.postType);
    if (postTypeFilter) {
      filter.postType = postTypeFilter;
    }
    if (query.q) {
      filter.content = { $regex: escapeRegex(query.q.trim()), $options: 'i' };
    }
    if (query.hashtag) {
      filter.hashtags = query.hashtag.trim().replace(/^#+/, '').toLowerCase();
    }
    const direction = resolveMongoSortDirection(query.sortOrder);
    const sortField = query.sortBy ?? 'createdAt';
    const sort = { pinnedToProfile: -1, [sortField]: direction } as Record<string, 1 | -1>;

    const profileShareCount = archivedOnly ? 0 : await this.countActiveProfileShares(userId);

    if (profileShareCount === 0) {
      const [items, total] = await Promise.all([
        this.postsRepository.findMany(filter, skip, limit, sort),
        this.postsRepository.count(filter),
      ]);
      const sharedPostIds = viewerUserId
        ? await this.findSharedPostIdSet(viewerUserId, items.map((item) => item.id))
        : new Set<string>();

      return buildPaginatedResponse(
        await Promise.all(
          items.map((item) => this.formatPostItemForViewer(item, sharedPostIds, viewerUserId)),
        ),
        {
          page,
          limit,
          total,
          offset: skip,
        },
      );
    }

    const originalTotal = await this.postsRepository.count(filter);
    const candidateLimit = skip + limit;
    const [originalPosts, profileShares] = await Promise.all([
      this.postsRepository.findMany(filter, 0, candidateLimit, sort),
      this.findActiveProfileShares(userId, candidateLimit),
    ]);
    const sharedPostIds = profileShares.map((share) => share.postId.toString());
    const sharedPosts = await this.postsRepository.findManyByIds(sharedPostIds);
    const postById = new Map(sharedPosts.map((post) => [post.id, post]));
    const visibleShares: Array<{ share: ProfileShareRecord; originalPost: PostDocument }> = [];

    const shareVisibility = await Promise.all(
      profileShares.map(async (share) => {
        const originalPost = postById.get(share.postId.toString());
        if (!originalPost || !this.postMatchesProfileQuery(originalPost, query)) return null;
        return (await this.canViewerSeePost(viewerUserId, originalPost))
          ? { share, originalPost }
          : null;
      }),
    );
    visibleShares.push(
      ...shareVisibility.filter(
        (item): item is { share: ProfileShareRecord; originalPost: PostDocument } => !!item,
      ),
    );

    const allPostIds = [
      ...originalPosts.map((item) => item.id),
      ...visibleShares.map((item) => item.originalPost.id),
    ];
    const viewerSharedPostIds = viewerUserId
      ? await this.findSharedPostIdSet(viewerUserId, allPostIds)
      : new Set<string>();
    const sharedByUser = await this.usersRepository.findById(userId);

    const timelineItems = [
      ...(await Promise.all(
        originalPosts.map(async (post) => ({
          sortDate: new Date((post as any).createdAt ?? 0).getTime(),
          item: await this.formatPostItemForViewer(post, viewerSharedPostIds, viewerUserId),
        })),
      )),
      ...visibleShares.map(({ share, originalPost }) => ({
        sortDate: new Date(share.createdAt ?? 0).getTime(),
        item: this.formatSharedPostItem(share, originalPost, sharedByUser, viewerSharedPostIds),
      })),
    ].sort((a, b) => b.sortDate - a.sortDate);

    const items = timelineItems.slice(skip, skip + limit).map((entry) => entry.item);
    const total = originalTotal + visibleShares.length;

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

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

    if (query.visibility) {
      filter.visibility = query.visibility;
    }
    if (query.postType) {
      filter.postType = query.postType;
    }
    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.hashtag?.trim()) {
      filter.hashtags = query.hashtag.trim().replace(/^#+/, '').toLowerCase();
    }
    if (query.moderationStatus) {
      filter.moderationStatus = query.moderationStatus;
    }

    const direction = resolveMongoSortDirection(query.sortOrder);
    const sortField = query.sortBy ?? 'createdAt';
    const sort = { [sortField]: direction } as Record<string, 1 | -1>;

    const [items, total] = await Promise.all([
      this.postsRepository.findManyAdmin(filter, skip, limit, sort),
      this.postsRepository.countAdmin(filter),
    ]);

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

  async createReel(
    userId: string,
    dto: CreateReelDto,
    videoFile?: { mimetype?: string; size: number; buffer: Buffer; originalname?: string },
    coverImageFile?: { mimetype?: string; size: number; buffer: Buffer; originalname?: string },
  ): Promise<PostDocument> {
    if (!videoFile && !dto.videoUrl) {
      throw new BadRequestException('Reel requires videoFile or videoUrl');
    }

    if (videoFile && dto.videoUrl) {
      throw new BadRequestException('Provide either videoFile or videoUrl for reel, not both');
    }

    return this.create(
      userId,
      {
        content: dto.content ?? '',
        contentTop: dto.contentTop,
        contentBottom: dto.contentBottom,
        videoUrl: dto.videoUrl,
        durationSeconds: dto.durationSeconds,
        thumbnailUrl: dto.thumbnailUrl,
        style: dto.style,
        maqam: dto.maqam,
        rhythmSignature: dto.rhythmSignature,
        mentionUsernames: dto.mentionUsernames,
        visibility: dto.visibility ?? PostVisibility.PUBLIC,
      },
      [],
      videoFile,
      undefined,
      coverImageFile,
    );
  }

  async findReels(query: ReelQueryDto, viewerUserId: string) {
    const page = query.page ?? 1;
    const limit = query.limit ?? 20;
    const skip = (page - 1) * limit;

    const [followingIds, invisibleUserIds] = await Promise.all([
      this.followsRepository.findFollowingIds(viewerUserId),
      this.findInvisibleUserIds(viewerUserId),
    ]);
    const unauthorizedPrivateAuthorIds = await this.usersRepository.findPrivateUserIds([
      viewerUserId,
      ...followingIds,
    ]);
    const excludedAuthorIds = Array.from(
      new Set([...invisibleUserIds, ...unauthorizedPrivateAuthorIds]),
    );
    const visibilityClauses: Record<string, unknown>[] = [
      { visibility: PostVisibility.PUBLIC },
      { authorId: new Types.ObjectId(viewerUserId) },
    ];
    if (followingIds.length) {
      visibilityClauses.push({
        visibility: PostVisibility.FOLLOWERS,
        authorId: { $in: followingIds.map((id) => new Types.ObjectId(id)) },
      });
    }
    const filter: Record<string, unknown> = {
      postType: PostType.VIDEO,
      ...(excludedAuthorIds.length
        ? { authorId: { $nin: excludedAuthorIds.map((id) => new Types.ObjectId(id)) } }
        : {}),
      $and: [{ $or: visibilityClauses }],
    };
    if (query.visibility) {
      (filter.$and as Record<string, unknown>[]).push({ visibility: query.visibility });
    }
    if (query.authorId) {
      filter.authorId = new Types.ObjectId(query.authorId);
    }
    if (query.q) {
      filter.content = { $regex: escapeRegex(query.q.trim()), $options: 'i' };
    }
    const direction = resolveMongoSortDirection(query.sortOrder);
    const sortField = query.sortBy ?? 'createdAt';
    const sort = { [sortField]: direction } as Record<string, 1 | -1>;

    const [items, total] = await Promise.all([
      this.postsRepository.findMany(filter, skip, limit, sort),
      this.postsRepository.count(filter),
    ]);

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

  async registerView(
    userId: string,
    postId: string,
  ): Promise<{ success: true; postId: string; viewCount: number }> {
    const post = await this.postsRepository.findById(postId);
    if (!post || !(await this.canViewerSeePost(userId, post))) {
      throw new NotFoundException('Post not found');
    }

    const shouldCount = await this.appCacheService.setIfAbsent(
      `post-view:${postId}:${userId}`,
      true,
      60 * 60,
    );
    if (shouldCount) {
      await this.postsRepository.incrementViewCount(postId, 1);
    }

    return {
      success: true,
      postId,
      viewCount: (post.viewCount ?? 0) + (shouldCount ? 1 : 0),
    };
  }

  async registerPlay(
    userId: string,
    postId: string,
  ): Promise<{ success: true; postId: string; playCount: number }> {
    const post = await this.postsRepository.findById(postId);
    if (!post || !(await this.canViewerSeePost(userId, post))) {
      throw new NotFoundException('Post not found');
    }

    if (post.postType !== PostType.AUDIO && post.postType !== PostType.VIDEO) {
      throw new BadRequestException('play counter is available only for audio or video posts');
    }

    const shouldCount = await this.appCacheService.setIfAbsent(
      `post-play:${postId}:${userId}`,
      true,
      15 * 60,
    );
    if (shouldCount) {
      await this.postsRepository.incrementPlayCount(postId, 1);
    }

    return {
      success: true,
      postId,
      playCount: (post.playCount ?? 0) + (shouldCount ? 1 : 0),
    };
  }

  async getShareSheet(userId: string, postId: string) {
    const post = await this.assertPostAvailableToViewer(userId, postId);
    const viewer = await this.usersRepository.findById(userId);
    if (!viewer || viewer.isDisabled) {
      throw new NotFoundException('User not found');
    }

    const suggestedFriends = await this.findSuggestedShareFriends(userId, 10);

    return {
      postId,
      shareUrl: this.buildPostShareUrl(postId),
      shareCount: post.shareCount ?? 0,
      viewer: this.formatShareViewer(viewer),
      suggestedFriends,
    };
  }

  async registerShare(
    userId: string,
    postId: string,
    dto: SharePostDto = {},
  ): Promise<{
    message: string;
    shareId: string;
    postId: string;
    shareCount: number;
    isSharedByMe: boolean;
    shareUrl: string;
  }> {
    const target = dto.target;
    if (target === PostShareTarget.FRIEND) {
      if (!dto.friendId) {
        throw new BadRequestException('friendId is required when target is friend');
      }
      const result = await this.sharePostToFriend(userId, postId, {
        friendId: dto.friendId,
        caption: dto.caption,
      });
      return {
        message: 'Post shared successfully',
        shareId: '',
        postId,
        shareCount: result.shareCount,
        isSharedByMe: false,
        shareUrl: this.buildPostShareUrl(postId),
      };
    }

    if (
      target === PostShareTarget.EXTERNAL ||
      target === PostShareTarget.COPY_LINK ||
      dto.channel === PostShareChannel.COPY_LINK
    ) {
      const post = await this.assertPostAvailableToViewer(userId, postId);
      const channel =
        target === PostShareTarget.COPY_LINK || dto.channel === PostShareChannel.COPY_LINK
          ? PostShareChannel.COPY_LINK
          : (dto.channel ?? PostShareChannel.OTHER);
      await this.recordShareEvent(userId, postId, {
        target: target ?? (channel === PostShareChannel.COPY_LINK ? PostShareTarget.COPY_LINK : PostShareTarget.EXTERNAL),
        channel,
        caption: dto.caption,
      });
      const shareCount = await this.incrementShareCountAndNotify(userId, postId, post);
      return {
        message: 'Post shared successfully',
        shareId: '',
        postId,
        shareCount,
        isSharedByMe: false,
        shareUrl: this.buildPostShareUrl(postId),
      };
    }

    const channel = dto.channel;
    const shareTarget = PostShareTarget.PROFILE;
    const post = await this.assertPostAvailableToViewer(userId, postId);
    const { share, created } = await this.createProfileShareRecord(userId, postId, post, {
      target: shareTarget,
      channel,
      caption: dto.caption,
    });
    const shareCount = created
      ? await this.incrementShareCountAndNotify(userId, postId, post)
      : (post.shareCount ?? 0);

    return {
      message: 'Post shared successfully',
      shareId: share._id.toString(),
      postId,
      shareCount,
      isSharedByMe: true,
      shareUrl: this.buildPostShareUrl(postId),
    };
  }

  async unsharePost(
    userId: string,
    postId: string,
  ): Promise<{ message: string; postId: string; shareCount: number; isSharedByMe: false }> {
    if (!Types.ObjectId.isValid(postId)) {
      throw new BadRequestException('Invalid post id');
    }

    const post = await this.postsRepository.findById(postId);
    if (!post) {
      throw new NotFoundException('Post not found');
    }

    const now = new Date();
    const result = await this.connection.collection<ProfileShareRecord>('postshares').findOneAndUpdate(
      this.activeProfileShareFilter(userId, postId),
      { $set: { isDeleted: true, deletedAt: now, updatedAt: now } },
      { returnDocument: 'after' },
    );

    let shareCount = post.shareCount ?? 0;
    if (result) {
      await this.postsRepository.incrementShareCount(postId, -1);
      await this.feedVersionService.bumpGlobalVersion();
      shareCount = Math.max(0, shareCount - 1);
    }

    return {
      message: 'Post unshared successfully',
      postId,
      shareCount,
      isSharedByMe: false,
    };
  }

  async sharePostToProfile(userId: string, postId: string, dto: SharePostToProfileDto) {
    await this.assertPostAvailableToViewer(userId, postId);
    const sharedPost = await this.createRepost(userId, postId, {
      content: dto.caption?.trim() ?? '',
      visibility: PostVisibility.PUBLIC,
    });
    const sourcePost = await this.postsRepository.findById(postId);
    await this.recordShareEvent(userId, postId, {
      target: PostShareTarget.PROFILE,
      caption: dto.caption,
      sharedPostId: sharedPost.id,
    });

    return {
      shared: true,
      shareCount: sourcePost?.shareCount ?? 0,
      post: sharedPost,
    };
  }

  async sharePostToFriend(userId: string, postId: string, dto: SharePostToFriendDto) {
    const post = await this.assertPostAvailableToViewer(userId, postId);
    const friend = await this.assertShareFriend(userId, dto.friendId);
    const conversation = await this.chatService.createConversation(userId, {
      participantIds: [dto.friendId],
      isGroup: false,
    });
    const shareUrl = this.buildPostShareUrl(postId);
    const caption = dto.caption?.trim();
    const content = [caption, shareUrl, `shared_post:${postId}`].filter(Boolean).join('\n\n');
    const message = await this.chatService.sendMessage(userId, {
      conversationId: conversation.id,
      content,
      messageType: 'text',
    });

    await this.recordShareEvent(userId, postId, {
      target: PostShareTarget.FRIEND,
      friendId: friend.id,
      caption: dto.caption,
      conversationId: conversation.id,
      messageId: message.id,
    });
    const shareCount = await this.incrementShareCountAndNotify(userId, postId, post);

    return {
      sent: true,
      shareCount,
      conversationId: conversation.id,
      messageId: message.id,
    };
  }

  async sharePostExternal(userId: string, postId: string, dto: SharePostExternalDto) {
    return this.registerShare(userId, postId, {
      target:
        dto.channel === PostShareChannel.COPY_LINK
          ? PostShareTarget.COPY_LINK
          : PostShareTarget.EXTERNAL,
      channel: dto.channel ?? PostShareChannel.OTHER,
    });
  }

  async updateCommentSettings(
    userId: string,
    postId: string,
    dto: UpdateCommentSettingsDto,
  ): Promise<PostDocument> {
    await this.assertPostOwner(userId, postId);
    const updated = await this.postsRepository.updateById(postId, {
      ...(typeof dto.commentsDisabled === 'boolean'
        ? { commentsDisabled: dto.commentsDisabled }
        : {}),
      ...(typeof dto.commentsFollowersOnly === 'boolean'
        ? { commentsFollowersOnly: dto.commentsFollowersOnly }
        : {}),
      ...(Array.isArray(dto.commentFilterKeywords)
        ? {
            commentFilterKeywords: Array.from(
              new Set(
                dto.commentFilterKeywords.map((item) => item.trim().toLowerCase()).filter(Boolean),
              ),
            ).slice(0, 50),
          }
        : {}),
    });
    if (!updated) {
      throw new NotFoundException('Post not found');
    }
    return updated;
  }

  async pinToProfile(userId: string, postId: string): Promise<PostDocument> {
    await this.assertPostOwner(userId, postId);
    const updated = await this.postsRepository.updateById(postId, { pinnedToProfile: true });
    if (!updated) {
      throw new NotFoundException('Post not found');
    }
    return updated;
  }

  async unpinFromProfile(userId: string, postId: string): Promise<PostDocument> {
    await this.assertPostOwner(userId, postId);
    const updated = await this.postsRepository.updateById(postId, { pinnedToProfile: false });
    if (!updated) {
      throw new NotFoundException('Post not found');
    }
    return updated;
  }

  async archive(userId: string, postId: string): Promise<PostDocument> {
    await this.assertPostOwner(userId, postId);
    const updated = await this.postsRepository.updateById(postId, {
      isArchived: true,
      pinnedToProfile: false,
    });
    if (!updated) {
      throw new NotFoundException('Post not found');
    }
    await this.feedVersionService.bumpGlobalVersion();
    return updated;
  }

  async restoreArchived(userId: string, postId: string): Promise<PostDocument> {
    await this.assertPostOwner(userId, postId);
    const updated = await this.postsRepository.updateById(postId, { isArchived: false });
    if (!updated) {
      throw new NotFoundException('Post not found');
    }
    await this.feedVersionService.bumpGlobalVersion();
    return updated;
  }

  async removeBySuperAdmin(superAdminIdentifier: string, postId: string): Promise<void> {
    const post = await this.postsRepository.findById(postId);
    if (!post) {
      throw new NotFoundException('Post not found');
    }

    const existingImageVariants = Array.isArray((post as any).imageVariants)
      ? (((post as any).imageVariants ?? []) as PostMediaVariantSet[])
      : [];
    const existingThumbnailVariants =
      ((post as any).thumbnailVariants as PostMediaVariantSet | null | undefined) ?? null;
    await this.postsRepository.deleteById(postId, superAdminIdentifier);
    await Promise.all([
      ...this.buildSavedImageAssets(post.imageUrls ?? [], existingImageVariants).map((asset) =>
        this.deleteSavedImageAsset(asset),
      ),
      this.deleteManagedPostMedia(post.videoUrl ?? ''),
      this.storageService.deleteContainingDirectory(post.hlsUrl ?? ''),
      this.deleteManagedPostMedia(post.audioUrl ?? ''),
      this.deleteThumbnailAsset(post.thumbnailUrl ?? '', existingThumbnailVariants),
    ]);
    const authorId = this.extractEntityId(post.authorId);
    if (authorId) {
      await this.usersRepository.incrementPostsCount(authorId, -1);
    }
    await this.feedVersionService.bumpGlobalVersion();
    await this.auditService.logSuperAdminAction(
      superAdminIdentifier,
      'post_delete',
      'post',
      postId,
      { authorId },
    );
  }

  async updateModerationStatusBySuperAdmin(
    superAdminIdentifier: string,
    postId: string,
    dto: { status: ModerationStatus; reason?: string },
  ): Promise<PostDocument> {
    const post = await this.postsRepository.findById(postId);
    if (!post) {
      throw new NotFoundException('Post not found');
    }

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

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

    return updated;
  }

  private resolvePostType(
    imageUrls: string[] = [],
    videoUrl?: string,
    audioUrl?: string,
  ): PostType {
    const hasImages = imageUrls.length > 0;
    const hasVideo = !!videoUrl?.trim();
    const hasAudio = !!audioUrl?.trim();

    if ((hasImages && hasVideo) || (hasImages && hasAudio) || (hasVideo && hasAudio)) {
      throw new BadRequestException('Post can contain either images, video, or audio');
    }

    if (hasImages) {
      return PostType.IMAGE;
    }

    if (hasVideo) {
      return PostType.VIDEO;
    }

    if (hasAudio) {
      return PostType.AUDIO;
    }

    return PostType.TEXT;
  }

  private resolvePostTypeFilter(input?: string): PostType | null {
    if (!input) {
      return null;
    }
    return input === 'reel' ? PostType.VIDEO : (input as PostType);
  }

  private normalizeMediaMetadata(
    dto: PostMediaMetadataInput,
    postType: PostType,
    fallback: NormalizedPostMediaMetadata = {
      durationSeconds: null,
      thumbnailUrl: '',
      style: '',
      maqam: '',
      rhythmSignature: '',
      waveformPeaks: [],
      waveformPeaksPreview: [],
      waveformPeaksDetailed: [],
    },
    options: {
      audioSourceBuffer?: Buffer;
      extractedDurationSeconds?: number | null;
      waveformSeed?: string;
      thumbnailUrl?: string;
    } = {},
  ): NormalizedPostMediaMetadata {
    const supportsMediaMetadata = postType === PostType.AUDIO || postType === PostType.VIDEO;
    const supportsWaveform = postType === PostType.AUDIO;

    if (!supportsMediaMetadata) {
      if (this.hasMediaMetadataInput(dto)) {
        throw new BadRequestException(
          'Audio/video metadata is allowed only for audio or video posts',
        );
      }
      return {
        durationSeconds: null,
        thumbnailUrl: '',
        style: '',
        maqam: '',
        rhythmSignature: '',
        waveformPeaks: [],
        waveformPeaksPreview: [],
        waveformPeaksDetailed: [],
      };
    }

    if (!supportsWaveform && typeof dto.waveformPeaks !== 'undefined') {
      throw new BadRequestException('waveformPeaks is allowed only for audio posts');
    }

    const durationSeconds =
      typeof options.extractedDurationSeconds === 'number'
        ? options.extractedDurationSeconds
        : typeof dto.durationSeconds === 'number'
          ? dto.durationSeconds
          : fallback.durationSeconds;
    const waveformSet = supportsWaveform
      ? this.resolveAudioWaveformPeaks(
          Array.isArray(dto.waveformPeaks) ? dto.waveformPeaks : undefined,
          options.audioSourceBuffer,
          options.waveformSeed,
          {
            waveformPeaks: fallback.waveformPeaks,
            waveformPeaksPreview: fallback.waveformPeaksPreview,
            waveformPeaksDetailed: fallback.waveformPeaksDetailed,
          },
          durationSeconds,
        )
      : { waveformPeaks: [], waveformPeaksPreview: [], waveformPeaksDetailed: [] };

    return {
      durationSeconds:
        durationSeconds,
      thumbnailUrl:
        typeof dto.thumbnailUrl === 'string'
          ? dto.thumbnailUrl.trim()
          : options.thumbnailUrl || fallback.thumbnailUrl,
      style: typeof dto.style === 'string' ? dto.style.trim() : fallback.style,
      maqam: typeof dto.maqam === 'string' ? dto.maqam.trim() : fallback.maqam,
      rhythmSignature:
        typeof dto.rhythmSignature === 'string'
          ? dto.rhythmSignature.trim()
          : fallback.rhythmSignature,
      ...waveformSet,
    };
  }

  private hasMediaMetadataInput(dto: PostMediaMetadataInput): boolean {
    return (
      typeof dto.durationSeconds === 'number' ||
      typeof dto.thumbnailUrl === 'string' ||
      typeof dto.style === 'string' ||
      typeof dto.maqam === 'string' ||
      typeof dto.rhythmSignature === 'string' ||
      typeof dto.waveformPeaks !== 'undefined'
    );
  }

  private extractMentions(content: string): string[] {
    const matches = content.match(/@[\p{L}\p{N}_.]+/gu) ?? [];
    const normalized = matches
      .map((item) => item.replace('@', '').trim().toLowerCase())
      .filter(Boolean);
    return Array.from(new Set(normalized)).slice(0, 30);
  }

  private combinePostText(content = '', contentTop = '', contentBottom = ''): string {
    return [contentTop, contentBottom, content]
      .map((value) => value.trim())
      .filter(Boolean)
      .join('\n');
  }

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

  private async resolveMentionTargets(
    explicitMentionUsernames: string[] | undefined,
    explicitMentionedUserIds: string[] | undefined,
    content: string,
    authorId: string,
  ): Promise<{
    mentionUsernames: string[];
    mentionedUserIds: Types.ObjectId[];
    mentionedUsers: MentionTarget[];
  }> {
    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');
    }

    const mentionedIds = this.normalizeMentionedUserIds(explicitMentionedUserIds, authorId);
    if (mergedMentionUsernames.length + mentionedIds.length > 30) {
      throw new BadRequestException('You can mention up to 30 users only');
    }

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

    const [usersByUsername, usersById] = await Promise.all([
      mergedMentionUsernames.length
        ? this.usersRepository.findByUsernames(mergedMentionUsernames)
        : Promise.resolve([]),
      mentionedIds.length
        ? this.usersRepository.findMany(
            { _id: { $in: mentionedIds }, isDisabled: false },
            0,
            mentionedIds.length,
          )
        : Promise.resolve([]),
    ]);
    const userByUsername = new Map(
      usersByUsername
        .filter((user) => !user.isDisabled)
        .map((user) => [
          user.username.toLowerCase(),
          this.toMentionTarget(user),
        ]),
    );

    const mentionedUsersById = new Map<string, MentionTarget>();
    for (const user of [
      ...mergedMentionUsernames
        .map((username) => userByUsername.get(username))
        .filter((user): user is MentionTarget => !!user),
      ...usersById.map((user) => this.toMentionTarget(user)),
    ]) {
      if (user.id !== authorId && user.username) {
        mentionedUsersById.set(user.id, user);
      }
    }

    const mentionedUsers = Array.from(mentionedUsersById.values()).slice(0, 30);

    return {
      mentionUsernames: mentionedUsers.map((user) => user.username).filter(Boolean),
      mentionedUserIds: mentionedUsers.map((user) => new Types.ObjectId(user.id)),
      mentionedUsers,
    };
  }

  private normalizeMentionedUserIds(input: string[] | undefined, authorId: string): string[] {
    return Array.from(
      new Set(
        (input ?? [])
          .map((id) => id?.trim())
          .filter((id): id is string => !!id && id !== authorId && Types.ObjectId.isValid(id)),
      ),
    );
  }

  private toMentionTarget(user: any): MentionTarget {
    return {
      id: user.id ?? user._id?.toString?.() ?? '',
      username: user.username?.toLowerCase?.() ?? '',
      name: user.name ?? '',
      stageName: user.stageName ?? '',
      avatar: user.avatar ?? '',
      isVerified: user.isVerified ?? false,
    };
  }

  private extractHashtags(content: string): string[] {
    const matches = content.match(/#[\p{L}\p{N}_]+/gu) ?? [];
    const normalized = matches
      .map((item) => item.replace('#', '').trim().toLowerCase())
      .filter(Boolean);

    return Array.from(new Set(normalized)).slice(0, 20);
  }

  private normalizeLocation(
    dto: Pick<CreatePostDto, 'location' | 'latitude' | 'longitude'>,
    fallback: { location: string; latitude: number | null; longitude: number | null } = {
      location: '',
      latitude: null,
      longitude: null,
    },
  ): { location: string; latitude: number | null; longitude: number | null } {
    const location = typeof dto.location === 'string' ? dto.location.trim() : fallback.location;
    const latitude = typeof dto.latitude === 'number' ? dto.latitude : fallback.latitude;
    const longitude = typeof dto.longitude === 'number' ? dto.longitude : fallback.longitude;

    const hasLatitude = typeof latitude === 'number';
    const hasLongitude = typeof longitude === 'number';
    if (hasLatitude !== hasLongitude) {
      throw new BadRequestException('latitude and longitude must be provided together');
    }

    return { location, latitude, longitude };
  }

  private buildImageItems(
    imageUrls: string[],
    captions: string[] | undefined,
    altTexts: string[] | undefined,
    fallback: PostImageItem[] = [],
  ): PostImageItem[] {
    return imageUrls.map((url, index) => ({
      url,
      caption:
        typeof captions?.[index] === 'string'
          ? captions[index].trim()
          : (fallback[index]?.caption ?? ''),
      altText:
        typeof altTexts?.[index] === 'string'
          ? altTexts[index].trim()
          : (fallback[index]?.altText ?? ''),
      order: index,
    }));
  }

  private async normalizeTaggedUserIds(
    input: string[] | undefined,
    authorId: string,
  ): Promise<Types.ObjectId[]> {
    if (!input?.length) {
      return [];
    }

    const unique = Array.from(
      new Set(
        input
          .map((item) => item?.trim())
          .filter((item): item is string => !!item)
          .filter((item) => item !== authorId),
      ),
    );

    if (unique.length > 20) {
      throw new BadRequestException('You can tag up to 20 users only');
    }

    if (unique.some((id) => !Types.ObjectId.isValid(id))) {
      throw new BadRequestException('Invalid tagged user id');
    }

    const rows = await this.usersRepository.findMany(
      {
        _id: { $in: unique.map((id) => new Types.ObjectId(id)) },
      },
      0,
      unique.length,
    );

    if (rows.length !== unique.length) {
      throw new BadRequestException('One or more tagged users do not exist');
    }

    return unique.map((id) => new Types.ObjectId(id));
  }

  private async normalizeUserIdList(
    input: string[] | undefined,
    currentUserId: string,
    maxCount: number,
    label: string,
  ): Promise<Types.ObjectId[]> {
    if (!input?.length) {
      return [];
    }

    const unique = Array.from(
      new Set(
        input
          .map((item) => item?.trim())
          .filter((item): item is string => !!item)
          .filter((item) => item !== currentUserId),
      ),
    );

    if (unique.length > maxCount) {
      throw new BadRequestException(`You can add up to ${maxCount} ${label}s only`);
    }

    if (unique.some((id) => !Types.ObjectId.isValid(id))) {
      throw new BadRequestException(`Invalid ${label} user id`);
    }

    const rows = await this.usersRepository.findMany(
      { _id: { $in: unique.map((id) => new Types.ObjectId(id)) } },
      0,
      unique.length,
    );

    if (rows.length !== unique.length) {
      throw new BadRequestException(`One or more ${label}s do not exist`);
    }

    return unique.map((id) => new Types.ObjectId(id));
  }

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

    await Promise.all(
      mentionedUsers.map(async (mentionedUser) => {
        try {
          await this.notificationsService.createMentionNotification(
            actorId,
            mentionedUser.id,
            postId,
            {
              resourceType: 'post',
              previewText: content.slice(0, 140),
            },
          );
        } catch (error) {
          this.logger.warn(
            `Mention notification failed for actor=${actorId} recipient=${mentionedUser.id}: ${
              error instanceof Error ? error.message : 'unknown error'
            }`,
          );
        }
      }),
    );
  }

  private async saveVideoUpload(file: UploadedVideoFile): Promise<SavedVideoUpload> {
    this.validateMediaFile('video', file);
    const optimized = await this.videoProcessingService.optimizeForPlayback(file);
    const extension = this.validateMediaFile('video', optimized.file);
    const durationSeconds = await this.mediaProbeService.extractDurationSecondsFromBuffer(
      optimized.file.buffer,
      optimized.file,
    );

    let videoUrl = '';
    let hlsUrl = '';
    let thumbnailUrl = '';
    let thumbnailVariants: PostMediaVariantSet | null = null;
    const hlsFolderName = `stream-${new Types.ObjectId().toString()}`;

    try {
      videoUrl = await this.storageService.saveFile({
        folderSegments: ['posts', 'video'],
        extension,
        buffer: optimized.file.buffer,
        contentType: optimized.file.mimetype,
        fileNamePrefix: 'video',
      });

      if (optimized.generatedHls?.files.length) {
        const savedHlsFiles = await this.storageService.saveFiles({
          folderSegments: ['posts', 'hls', hlsFolderName],
          files: optimized.generatedHls.files,
        });
        hlsUrl = savedHlsFiles[optimized.generatedHls.playlistRelativePath] ?? '';
      }

      if (optimized.generatedThumbnail) {
        const savedThumbnail = await this.saveResponsiveImageAsset('thumbnails', {
          buffer: optimized.generatedThumbnail.buffer,
          size: optimized.generatedThumbnail.buffer.length,
          mimetype: optimized.generatedThumbnail.contentType,
          originalname: `thumbnail${optimized.generatedThumbnail.extension}`,
        });
        thumbnailUrl = savedThumbnail.primaryUrl;
        thumbnailVariants = savedThumbnail.variants;
      }

      return { videoUrl, hlsUrl, thumbnailUrl, thumbnailVariants, durationSeconds };
    } catch (error) {
      await Promise.all([
        videoUrl ? this.deleteManagedPostMedia(videoUrl) : Promise.resolve(),
        hlsUrl ? this.storageService.deleteContainingDirectory(hlsUrl) : Promise.resolve(),
        thumbnailUrl
          ? this.deleteThumbnailAsset(thumbnailUrl, thumbnailVariants)
          : Promise.resolve(),
      ]);
      throw error;
    }
  }

  async createRepost(
    userId: string,
    sourcePostId: string,
    dto: CreateRepostDto,
  ): Promise<PostDocument> {
    const sourcePost = await this.postsRepository.findById(sourcePostId);
    if (!sourcePost) {
      throw new NotFoundException('Source post not found');
    }
    if (!(await this.canViewerSeePost(userId, sourcePost))) {
      throw new NotFoundException('Source post not found');
    }
    if (this.extractEntityId(sourcePost.authorId) === userId && !dto.content?.trim()) {
      throw new BadRequestException('You cannot repost your own post without a quote');
    }

    const content = dto.content?.trim() ?? '';
    const mentionResolution = await this.resolveMentionTargets(undefined, undefined, content, userId);
    const hashtags = this.extractHashtags(content);
    const post = await this.postsRepository.create(userId, {
      content,
      postType: PostType.TEXT,
      processingStatus: ProcessingStatus.READY,
      visibility: dto.visibility ?? PostVisibility.PUBLIC,
      repostOfPostId: content ? null : new Types.ObjectId(sourcePostId),
      quoteOfPostId: content ? new Types.ObjectId(sourcePostId) : null,
      mentionUsernames: mentionResolution.mentionUsernames,
      mentionedUserIds: mentionResolution.mentionedUserIds,
      hashtags,
    });

    await this.usersRepository.incrementPostsCount(userId, 1);
    await this.postsRepository.incrementShareCount(sourcePostId, 1);
    await this.feedVersionService.bumpGlobalVersion();
    await this.notifyMentionedUsers(userId, post.id, mentionResolution.mentionedUsers, content);

    const populated = await this.postsRepository.findById(post.id);
    return populated ?? post;
  }

  private async saveMediaFile(
    mediaType: 'image' | 'video' | 'audio',
    file: { mimetype?: string; size: number; buffer: Buffer; originalname?: string },
  ): Promise<string> {
    const extension = this.validateMediaFile(mediaType, file);
    const folder = mediaType === 'image' ? 'images' : mediaType === 'video' ? 'video' : 'audio';
    return this.storageService.saveFile({
      folderSegments: ['posts', folder],
      extension,
      buffer: file.buffer,
      contentType: file.mimetype,
      fileNamePrefix: mediaType,
    });
  }

  private validateMediaFile(
    mediaType: 'image' | 'video' | 'audio',
    file: { mimetype?: string; size: number; buffer: Buffer; originalname?: string },
  ): string {
    return assertAllowedMediaFile(file, mediaType, {
      invalidMessage:
        mediaType === 'image'
          ? 'imageFiles must be jpg, jpeg, png, or webp'
          : mediaType === 'video'
            ? 'videoFile must be mp4, mov, or webm'
            : 'audioFile must be mp3, wav, m4a, aac, ogg, or webm',
      maxSizeBytes:
        mediaType === 'image'
          ? MEDIA_MAX_SIZE_BYTES.postsImage
          : mediaType === 'video'
            ? MEDIA_MAX_SIZE_BYTES.postsVideo
            : MEDIA_MAX_SIZE_BYTES.postsAudio,
      maxSizeMessage:
        mediaType === 'image'
          ? 'Each image must be 10MB or less'
          : mediaType === 'video'
            ? 'videoFile size must be 100MB or less'
            : 'audioFile size must be 20MB or less',
    });
  }

  private async saveImageFiles(
    files: Array<{ mimetype?: string; size: number; buffer: Buffer; originalname?: string }>,
  ): Promise<SavedImageUpload[]> {
    if (!files.length) {
      return [];
    }
    if (files.length > 10) {
      throw new BadRequestException('Post can contain up to 10 images');
    }

    const uploads: SavedImageUpload[] = [];
    try {
      for (const file of files) {
        uploads.push(await this.saveResponsiveImageAsset('images', file));
      }
      return uploads;
    } catch (error) {
      await Promise.all(uploads.map((upload) => this.deleteSavedImageAsset(upload)));
      throw error;
    }
  }

  private async saveResponsiveImageAsset(
    folder: 'images' | 'thumbnails',
    file: UploadedImageFile,
  ): Promise<SavedImageUpload> {
    this.validateMediaFile('image', file);
    const processed = await this.imageProcessingService.processForResponsiveDelivery(file);
    const groupName = `${folder.slice(0, -1)}-${new Types.ObjectId().toString()}`;
    const savedFiles = await this.storageService.saveFiles({
      folderSegments: ['posts', folder, groupName],
      files: processed.variants.map((variant) => ({
        relativePath: variant.relativePath,
        buffer: variant.buffer,
        contentType: variant.contentType,
      })),
    });

    const variants = this.buildVariantSet(processed, savedFiles);
    return {
      primaryUrl: this.resolvePrimaryVariantUrl(variants, processed.primaryVariantName),
      variants,
    };
  }

  private buildVariantSet(
    processed: Awaited<ReturnType<ImageProcessingService['processForResponsiveDelivery']>>,
    savedFiles: Record<string, string>,
  ): PostMediaVariantSet {
    const byName = new Map(
      processed.variants.map((variant) => [variant.name, savedFiles[variant.relativePath] ?? '']),
    );
    const originalUrl = byName.get('original') ?? '';
    const lowUrl = byName.get('low') ?? originalUrl;
    const mediumUrl = byName.get('medium') ?? byName.get('high') ?? lowUrl;
    const highUrl = byName.get('high') ?? mediumUrl ?? lowUrl;

    return {
      originalUrl,
      lowUrl,
      mediumUrl,
      highUrl,
    };
  }

  private resolvePrimaryVariantUrl(
    variants: PostMediaVariantSet,
    preferredVariantName: 'original' | 'low' | 'medium' | 'high',
  ): string {
    const candidates =
      preferredVariantName === 'low'
        ? [variants.lowUrl, variants.mediumUrl, variants.highUrl, variants.originalUrl]
        : preferredVariantName === 'high'
          ? [variants.highUrl, variants.mediumUrl, variants.lowUrl, variants.originalUrl]
          : preferredVariantName === 'original'
            ? [variants.originalUrl, variants.highUrl, variants.mediumUrl, variants.lowUrl]
            : [variants.mediumUrl, variants.highUrl, variants.lowUrl, variants.originalUrl];

    return candidates.find((candidate) => !!candidate) ?? '';
  }

  private buildSavedImageAssets(
    imageUrls: string[],
    imageVariants: PostMediaVariantSet[] = [],
  ): SavedImageUpload[] {
    return imageUrls.map((primaryUrl, index) => ({
      primaryUrl,
      variants: imageVariants[index] ?? this.buildFlatVariantSet(primaryUrl),
    }));
  }

  private buildFlatVariantSet(primaryUrl: string): PostMediaVariantSet {
    return {
      originalUrl: primaryUrl,
      lowUrl: primaryUrl,
      mediumUrl: primaryUrl,
      highUrl: primaryUrl,
    };
  }

  private async deleteSavedImageAsset(upload: SavedImageUpload): Promise<void> {
    const anchorUrl =
      upload.variants.mediumUrl ||
      upload.variants.highUrl ||
      upload.variants.lowUrl ||
      upload.variants.originalUrl ||
      upload.primaryUrl;

    const hasManagedVariantGroup = [
      upload.variants.originalUrl,
      upload.variants.lowUrl,
      upload.variants.mediumUrl,
      upload.variants.highUrl,
    ].some((url) => !!url && url !== upload.primaryUrl);

    if (anchorUrl && hasManagedVariantGroup) {
      await this.storageService.deleteContainingDirectory(anchorUrl);
      return;
    }

    await this.deleteManagedPostMedia(upload.primaryUrl);
  }

  private async deleteThumbnailAsset(
    thumbnailUrl: string,
    thumbnailVariants: PostMediaVariantSet | null,
  ): Promise<void> {
    if (!thumbnailUrl) {
      return;
    }

    const anchorUrl =
      thumbnailVariants?.mediumUrl ||
      thumbnailVariants?.highUrl ||
      thumbnailVariants?.lowUrl ||
      thumbnailVariants?.originalUrl ||
      '';

    const hasManagedVariantGroup =
      !!thumbnailVariants &&
      [
        thumbnailVariants.originalUrl,
        thumbnailVariants.lowUrl,
        thumbnailVariants.mediumUrl,
        thumbnailVariants.highUrl,
      ].some((url) => !!url && url !== thumbnailUrl);

    if (anchorUrl && hasManagedVariantGroup) {
      await this.storageService.deleteContainingDirectory(anchorUrl);
      return;
    }

    await this.deleteManagedPostMedia(thumbnailUrl);
  }

  private async deleteManagedPostMedia(fileUrl: string): Promise<void> {
    await this.storageService.deleteFile(fileUrl);
  }

  private resolveAudioWaveformPeaks(
    providedPeaks: number[] | undefined,
    sourceBuffer: Buffer | undefined,
    waveformSeed: string | undefined,
    fallbackPeaks: Partial<WaveformPeakSet> = {},
    durationSeconds?: number | null,
  ): WaveformPeakSet {
    if (Array.isArray(providedPeaks) && providedPeaks.length) {
      return buildWaveformPeakSet(providedPeaks, durationSeconds);
    }

    if (sourceBuffer?.length) {
      return generateWaveformPeakSetFromBuffer(sourceBuffer, durationSeconds);
    }

    if (fallbackPeaks.waveformPeaksDetailed?.length) {
      return buildWaveformPeakSet(fallbackPeaks.waveformPeaksDetailed, durationSeconds);
    }

    if (fallbackPeaks.waveformPeaksPreview?.length) {
      return buildWaveformPeakSet(fallbackPeaks.waveformPeaksPreview, durationSeconds);
    }

    if (fallbackPeaks.waveformPeaks?.length) {
      return buildWaveformPeakSet(fallbackPeaks.waveformPeaks, durationSeconds);
    }

    return generateWaveformPeakSetFromSeed(waveformSeed ?? 'audio-post', durationSeconds);
  }

  private async assertPostOwner(userId: string, postId: string): Promise<PostDocument> {
    const post = await this.postsRepository.findById(postId);
    if (!post) {
      throw new NotFoundException('Post not found');
    }
    if (this.extractEntityId(post.authorId) !== userId) {
      throw new ForbiddenException('You can update only your own posts');
    }
    return post;
  }

  private async assertPostAvailableToViewer(userId: string, postId: string): Promise<PostDocument> {
    if (!Types.ObjectId.isValid(postId)) {
      throw new BadRequestException('Invalid post id');
    }

    const post = await this.postsRepository.findById(postId);
    if (!post || post.isArchived || post.moderationStatus === ModerationStatus.HIDDEN) {
      throw new NotFoundException('Post not found');
    }

    const authorId = this.extractEntityId(post.authorId);
    if (!authorId) {
      throw new NotFoundException('Post not found');
    }

    const author = await this.usersRepository.findById(authorId);
    if (!author || author.isDisabled) {
      throw new NotFoundException('Post not found');
    }

    if (authorId === userId) {
      return post;
    }

    if (await this.hasBlockBetween(userId, authorId)) {
      throw new ForbiddenException('You cannot access this post');
    }

    if (author.isPrivate && !(await this.isFollowing(userId, authorId))) {
      throw new ForbiddenException('You cannot access this post');
    }

    if (post.visibility === PostVisibility.PRIVATE) {
      throw new ForbiddenException('You cannot access this post');
    }

    if (post.visibility === PostVisibility.FOLLOWERS) {
      const following = await this.isFollowing(userId, authorId);
      if (!following) {
        throw new ForbiddenException('You cannot access this post');
      }
    }

    return post;
  }

  private async assertShareFriend(userId: string, friendId: string) {
    if (!Types.ObjectId.isValid(friendId)) {
      throw new BadRequestException('Invalid friend id');
    }
    if (friendId === userId) {
      throw new BadRequestException('friendId cannot be the current user');
    }

    const friend = await this.usersRepository.findById(friendId);
    if (!friend || friend.isDisabled) {
      throw new NotFoundException('Friend not found');
    }

    const [globalBlock, chatBlock] = await Promise.all([
      this.blocksRepository.findAnyBetween(userId, friendId),
      this.connection.collection('chatblocks').countDocuments({
        $or: [
          { blockerId: new Types.ObjectId(userId), blockedId: new Types.ObjectId(friendId) },
          { blockerId: new Types.ObjectId(friendId), blockedId: new Types.ObjectId(userId) },
        ],
      }),
    ]);
    if (globalBlock || chatBlock > 0) {
      throw new ForbiddenException('You cannot share with this user');
    }

    return friend;
  }

  private async incrementShareCountAndNotify(
    userId: string,
    postId: string,
    post: PostDocument,
  ): Promise<number> {
    await this.postsRepository.incrementShareCount(postId, 1);
    await this.feedVersionService.bumpGlobalVersion();

    const authorId = this.extractEntityId(post.authorId);
    if (authorId && authorId !== userId) {
      try {
        await this.notificationsService.createShareNotification(userId, authorId, postId, {
          resourceType: 'post',
          previewText: (post.content ?? '').slice(0, 140),
        });
      } catch (error) {
        this.logger.warn(
          `Share notification failed for actor=${userId} recipient=${authorId}: ${
            error instanceof Error ? error.message : 'unknown error'
          }`,
        );
      }
    }

    return (post.shareCount ?? 0) + 1;
  }

  private activeProfileShareFilter(userId: string, postId?: string) {
    return {
      userId: new Types.ObjectId(userId),
      ...(postId ? { postId: new Types.ObjectId(postId) } : {}),
      target: PostShareTarget.PROFILE,
      $or: [{ isDeleted: false }, { isDeleted: { $exists: false } }],
      deletedAt: null,
    };
  }

  private async createProfileShareRecord(
    userId: string,
    postId: string,
    post: PostDocument,
    payload: { target?: PostShareTarget; channel?: PostShareChannel; caption?: string },
  ): Promise<{ share: ProfileShareRecord; created: boolean }> {
    const existing = await this.connection
      .collection<ProfileShareRecord>('postshares')
      .findOne(this.activeProfileShareFilter(userId, postId));
    if (existing) {
      return { share: existing, created: false };
    }

    const authorId = this.extractEntityId(post.authorId);
    const now = new Date();
    const share = {
      userId: new Types.ObjectId(userId),
      postId: new Types.ObjectId(postId),
      originalAuthorId: new Types.ObjectId(authorId),
      sharedFrom: {
        type: 'user',
        id: authorId,
        name: this.formatUserDisplayName(post.authorId),
      },
      target: PostShareTarget.PROFILE,
      channel: payload.channel ?? '',
      caption: payload.caption?.trim() ?? '',
      shareUrl: this.buildPostShareUrl(postId),
      friendId: null,
      sharedPostId: null,
      conversationId: null,
      messageId: null,
      isDeleted: false,
      deletedAt: null,
      createdAt: now,
      updatedAt: now,
    };

    try {
      const result = await this.connection.collection('postshares').insertOne(share);
      return { share: { ...share, _id: result.insertedId }, created: true };
    } catch (error: any) {
      if (error?.code !== 11000) {
        throw error;
      }
      const duplicate = await this.connection
        .collection<ProfileShareRecord>('postshares')
        .findOne(this.activeProfileShareFilter(userId, postId));
      if (!duplicate) {
        throw error;
      }
      return { share: duplicate, created: false };
    }
  }

  private async countActiveProfileShares(userId: string): Promise<number> {
    return this.connection
      .collection('postshares')
      .countDocuments(this.activeProfileShareFilter(userId));
  }

  private async findActiveProfileShares(userId: string, limit: number): Promise<ProfileShareRecord[]> {
    return this.connection
      .collection<ProfileShareRecord>('postshares')
      .find(this.activeProfileShareFilter(userId))
      .sort({ createdAt: -1 })
      .limit(limit)
      .toArray();
  }

  private async findSharedPostIdSet(userId: string, postIds: string[]): Promise<Set<string>> {
    const validPostIds = Array.from(new Set(postIds.filter((id) => Types.ObjectId.isValid(id))));
    if (!Types.ObjectId.isValid(userId) || validPostIds.length === 0) {
      return new Set<string>();
    }

    const rows = await this.connection
      .collection<ProfileShareRecord>('postshares')
      .find({
        ...this.activeProfileShareFilter(userId),
        postId: { $in: validPostIds.map((id) => new Types.ObjectId(id)) },
      })
      .project({ postId: 1 })
      .toArray();

    return new Set(rows.map((row) => row.postId.toString()));
  }

  private formatPostItem(post: PostDocument | Record<string, any>, sharedPostIds: Set<string>) {
    const item =
      post && typeof (post as any).toObject === 'function'
        ? ((post as any).toObject() as Record<string, unknown>)
        : ({ ...post } as Record<string, unknown>);
    const postId = this.extractEntityId(item._id ?? item.id);

    return {
      type: 'post',
      ...item,
      id: postId,
      shareCount: Number(item.shareCount ?? 0),
      isSharedByMe: sharedPostIds.has(postId),
    };
  }

  private formatSharedPostItem(
    share: ProfileShareRecord,
    originalPost: PostDocument,
    sharedByUser: unknown,
    sharedPostIds: Set<string>,
  ) {
    const originalAuthor = (originalPost as any).authorId;

    return {
      id: share._id.toString(),
      type: 'shared_post',
      sharedAt: (share.createdAt ?? new Date(0)).toISOString(),
      sharedBy: this.formatUserSummary(sharedByUser),
      sharedFrom: this.formatUserSummary(originalAuthor),
      originalPost: this.formatPostItem(originalPost, sharedPostIds),
    };
  }

  private formatUserSummary(user: unknown) {
    const userObject =
      user && typeof (user as any).toObject === 'function'
        ? (user as any).toObject()
        : ((user ?? {}) as Record<string, any>);
    const id = this.extractEntityId(userObject);

    return {
      id,
      name: userObject.name ?? '',
      username: userObject.username ?? '',
      avatar: this.resolveAvatarUrl(userObject.avatar),
    };
  }

  private formatUserDisplayName(user: unknown): string {
    const summary = this.formatUserSummary(user);
    return summary.name || summary.username || summary.id;
  }

  private postMatchesProfileQuery(post: PostDocument, query: PostQueryDto): boolean {
    const postTypeFilter = this.resolvePostTypeFilter(query.mediaType ?? query.postType);
    if (postTypeFilter && post.postType !== postTypeFilter) {
      return false;
    }

    if (query.q?.trim()) {
      const needle = query.q.trim().toLowerCase();
      const content = this.combinePostText(
        post.content ?? '',
        (post as any).contentTop ?? '',
        (post as any).contentBottom ?? '',
      ).toLowerCase();
      if (!content.includes(needle)) {
        return false;
      }
    }

    if (query.hashtag?.trim()) {
      const hashtag = query.hashtag.trim().replace(/^#+/, '').toLowerCase();
      if (!(post.hashtags ?? []).includes(hashtag)) {
        return false;
      }
    }

    return true;
  }

  async canViewerSeePost(viewerUserId: string | undefined, post: PostDocument): Promise<boolean> {
    if (post.isDeleted || post.isArchived || post.moderationStatus === ModerationStatus.HIDDEN) {
      return false;
    }

    const authorId = this.extractEntityId(post.authorId);
    if (!authorId) {
      return false;
    }
    const author = await this.usersRepository.findById(authorId);
    if (!author || author.isDisabled) {
      return false;
    }
    if (viewerUserId && viewerUserId === authorId) {
      return true;
    }
    if (viewerUserId && (await this.hasBlockBetween(viewerUserId, authorId))) {
      return false;
    }
    if (
      author.isPrivate &&
      (!viewerUserId || !(await this.isFollowing(viewerUserId, authorId)))
    ) {
      return false;
    }
    if (post.visibility === PostVisibility.PRIVATE) {
      return false;
    }
    if (post.visibility === PostVisibility.FOLLOWERS) {
      return !!viewerUserId && (await this.isFollowing(viewerUserId, authorId));
    }
    return true;
  }

  private async formatPostItemForViewer(
    post: PostDocument | Record<string, any>,
    sharedPostIds: Set<string>,
    viewerUserId?: string,
  ): Promise<Record<string, unknown>> {
    const item = this.formatPostItem(post, sharedPostIds) as Record<string, any>;
    await Promise.all(
      (['repostOfPostId', 'quoteOfPostId'] as const).map(async (field) => {
        const original = (post as any)[field];
        if (!original) {
          return;
        }
        if (
          typeof original !== 'object' ||
          original instanceof Types.ObjectId ||
          !('visibility' in original)
        ) {
          item[field] = null;
          return;
        }
        if (!(await this.canViewerSeePost(viewerUserId, original as PostDocument))) {
          item[field] = null;
        }
      }),
    );
    return item;
  }

  private async findInvisibleUserIds(userId: string): Promise<string[]> {
    const objectId = new Types.ObjectId(userId);
    const rows = await this.connection
      .collection('blocks')
      .find({
        $or: [{ blockerId: objectId }, { blockedId: objectId }],
      })
      .project({ blockerId: 1, blockedId: 1 })
      .toArray();
    return rows.map((row) =>
      row.blockerId?.toString() === userId
        ? row.blockedId?.toString()
        : row.blockerId?.toString(),
    ).filter((id): id is string => !!id);
  }

  private async recordShareEvent(
    userId: string,
    postId: string,
    payload: {
      target?: PostShareTarget;
      channel?: PostShareChannel;
      caption?: string;
      friendId?: string;
      sharedPostId?: string;
      conversationId?: string;
      messageId?: string;
    },
  ): Promise<void> {
    await this.connection.collection('postshares').insertOne({
      postId: new Types.ObjectId(postId),
      userId: new Types.ObjectId(userId),
      target: payload.target ?? 'legacy',
      channel: payload.channel ?? '',
      caption: payload.caption?.trim() ?? '',
      friendId:
        payload.friendId && Types.ObjectId.isValid(payload.friendId)
          ? new Types.ObjectId(payload.friendId)
          : null,
      sharedPostId:
        payload.sharedPostId && Types.ObjectId.isValid(payload.sharedPostId)
          ? new Types.ObjectId(payload.sharedPostId)
          : null,
      conversationId:
        payload.conversationId && Types.ObjectId.isValid(payload.conversationId)
          ? new Types.ObjectId(payload.conversationId)
          : null,
      messageId:
        payload.messageId && Types.ObjectId.isValid(payload.messageId)
          ? new Types.ObjectId(payload.messageId)
          : null,
      shareUrl: this.buildPostShareUrl(postId),
      createdAt: new Date(),
    });
  }

  private async findSuggestedShareFriends(userId: string, limit: number) {
    const blockedIds = new Set(await this.blocksRepository.findBlockingOrBlockedIds(userId));
    const rows = await this.followsRepository.findMany(
      { followerId: new Types.ObjectId(userId) },
      0,
      Math.max(limit * 2, limit),
      { createdAt: -1 },
    );

    const items = rows
      .map((row: any) => row.followingId)
      .filter((candidate: any) => candidate && typeof candidate === 'object')
      .filter((candidate: any) => {
        const id = this.extractEntityId(candidate);
        return id && id !== userId && !blockedIds.has(id) && !candidate.isDisabled;
      })
      .map((candidate: any) => this.formatShareFriend(candidate, true));

    return this.uniqueById(items).slice(0, limit);
  }

  private formatShareViewer(user: any) {
    const userObject = typeof user.toObject === 'function' ? user.toObject() : user;
    const id = this.extractEntityId(userObject);
    return {
      id,
      username: userObject.username ?? '',
      name: userObject.name ?? '',
      stageName: userObject.stageName ?? '',
      avatar: this.resolveAvatarUrl(userObject.avatar),
    };
  }

  private formatShareFriend(user: any, isFollowing: boolean) {
    const userObject = typeof user.toObject === 'function' ? user.toObject() : user;
    const id = this.extractEntityId(userObject);
    const stageName = typeof userObject.stageName === 'string' ? userObject.stageName.trim() : '';
    const name = typeof userObject.name === 'string' ? userObject.name.trim() : '';
    const username = typeof userObject.username === 'string' ? userObject.username.trim() : '';
    return {
      id,
      displayName: stageName || name || username,
      username,
      avatarUrl: this.resolveAvatarUrl(userObject.avatar),
      isVerified: !!userObject.isVerified,
      isFollowing,
    };
  }

  private uniqueById<T extends { id: string }>(items: T[]): T[] {
    const seen = new Set<string>();
    return items.filter((item) => {
      if (!item.id || seen.has(item.id)) {
        return false;
      }
      seen.add(item.id);
      return true;
    });
  }

  private buildPostShareUrl(postId: string): string {
    return buildPostShareUrl(this.resolveShareBaseUrl(), postId);
  }

  private resolveShareBaseUrl(): string {
    return resolveShareBaseUrl(this.configService);
  }

  private resolveAvatarUrl(value: unknown): string {
    const resolved = resolveManagedFileUrl(typeof value === 'string' ? value : '');
    return typeof resolved === 'string' ? resolved : '';
  }

  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 async isFollowing(followerId: string, followingId: string): Promise<boolean> {
    if (!Types.ObjectId.isValid(followerId) || !Types.ObjectId.isValid(followingId)) {
      return false;
    }
    const count = await this.connection.collection('follows').countDocuments({
      followerId: new Types.ObjectId(followerId),
      followingId: new Types.ObjectId(followingId),
    });
    return count > 0;
  }

  private async hasBlockBetween(userA: string, userB: string): Promise<boolean> {
    if (!Types.ObjectId.isValid(userA) || !Types.ObjectId.isValid(userB)) {
      return false;
    }
    const userAId = new Types.ObjectId(userA);
    const userBId = new Types.ObjectId(userB);
    const count = await this.connection.collection('blocks').countDocuments({
      $or: [
        { blockerId: userAId, blockedId: userBId },
        { blockerId: userBId, blockedId: userAId },
      ],
    });
    return count > 0;
  }
}
