import { Injectable, Logger, NotFoundException } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { InjectConnection } from '@nestjs/mongoose';
import { Connection, Types } from 'mongoose';
import { performance } from 'perf_hooks';
import { PostType } from '../../common/enums/post-type.enum';
import { PostVisibility } from '../../common/enums/post-visibility.enum';
import { decodeOffsetCursor, encodeOffsetCursor } from '../../common/utils/cursor.util';
import { buildPaginatedResponse } from '../../common/utils/pagination.util';
import { AppCacheService } from '../../infrastructure/cache/app-cache.service';
import { FeedVersionService } from '../../infrastructure/cache/feed-version.service';
import { BlocksService } from '../blocks/blocks.service';
import { FollowsService } from '../follows/follows.service';
import { LikesRepository } from '../likes/likes.repository';
import { MarketplaceService } from '../marketplace/marketplace.service';
import { PostShareTarget } from '../posts/dto/share-post.dto';
import { SavesRepository } from '../saves/saves.repository';
import { UserDocument } from '../users/schemas/user.schema';
import { UsersRepository } from '../users/users.repository';
import { FeedPostTypeFilter, FeedQueryDto } from './dto/feed-query.dto';
import { FeedRepository } from './feed.repository';

type FeedPostItem = Record<string, unknown> & {
  feedItemType: 'post';
  feedScore?: number;
  likedByMe: boolean;
  savedByMe: boolean;
  isSharedByMe: boolean;
  followingAuthor: boolean;
  isOwnPost: boolean;
  canComment: boolean;
  canMessage: boolean;
  engagement: {
    likesCount: number;
    commentsCount: number;
    savesCount: number;
    shareCount: number;
    viewCount: number;
    playCount: number;
  };
};

type FeedCardItem =
  | {
      id: string;
      feedItemType: 'suggested_users';
      title: string;
      subtitle: string;
      items: Array<Record<string, unknown>>;
    }
  | {
      id: string;
      feedItemType: 'featured_marketplace';
      title: string;
      subtitle: string;
      listings: Array<Record<string, unknown>>;
      musicalInstruments: Array<Record<string, unknown>>;
      instruments: Array<Record<string, unknown>>;
      repairShops: Array<Record<string, unknown>>;
    };

type ViewerRankingProfile = {
  authorAffinity: Record<string, number>;
  postTypeAffinity: Record<string, number>;
  hashtagAffinity: Record<string, number>;
  interactedPostIds: string[];
  engagementSignals: Record<string, { watchMs: number; completions: number; rewatches: number; profileOpens: number }>;
};

type RankedPost = {
  post: Record<string, any> & { toObject(): unknown };
  score: number;
};

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

  constructor(
    private readonly feedRepository: FeedRepository,
    private readonly usersRepository: UsersRepository,
    private readonly cacheService: AppCacheService,
    private readonly feedVersionService: FeedVersionService,
    private readonly configService: ConfigService,
    private readonly likesRepository: LikesRepository,
    private readonly savesRepository: SavesRepository,
    private readonly followsService: FollowsService,
    private readonly marketplaceService: MarketplaceService,
    private readonly blocksService: BlocksService,
    @InjectConnection() private readonly connection: Connection,
  ) {}

  async getMyFeed(currentUserId: string, query: FeedQueryDto) {
    const timing = this.startTiming('feed.me');
    const followingOnly = query.followingOnly ?? true;
    const cacheEnabled =
      this.configService.get<boolean>('feedCache.enabled', { infer: true }) ?? true;
    const [globalVersion, userVersion] = cacheEnabled
      ? await Promise.all([
          this.feedVersionService.getGlobalVersion(),
          this.feedVersionService.getUserVersion(currentUserId),
        ])
      : [0, 0];
    const includeSuggestions = this.shouldIncludeSuggestions(query);
    const cacheKey = this.buildCacheKey('me', {
      currentUserId,
      globalVersion,
      userVersion,
      page: query.page ?? 1,
      limit: query.limit ?? 20,
      cursor: query.cursor ?? '',
      followingOnly,
      radiusKm: query.radiusKm ?? 30,
      preferredPostType: query.preferredPostType ?? '',
      includeSuggestions,
      suggestionInterval: query.suggestionInterval ?? 4,
    });
    let cacheFillLock: Awaited<ReturnType<AppCacheService['acquireFillLock']>> | null = null;
    if (cacheEnabled) {
      const cached = await this.cacheService.get<Record<string, unknown>>(cacheKey);
      if (cached) {
        this.logFeedTiming(timing, {
          cacheHit: true,
          itemCount: Array.isArray(cached.items) ? cached.items.length : undefined,
          nextCursor: cached.nextCursor ?? null,
          responseBytes: this.measureResponseBytes(cached),
        });
        return cached;
      }
      cacheFillLock = await this.cacheService.acquireFillLock(cacheKey, 10);
      if (!cacheFillLock.acquired) {
        const coalesced = await this.cacheService.waitForValue<Record<string, unknown>>(
          cacheKey,
          2_000,
        );
        if (coalesced) {
          this.logFeedTiming(timing, {
            cacheHit: true,
            cacheCoalesced: true,
            itemCount: Array.isArray(coalesced.items) ? coalesced.items.length : undefined,
            responseBytes: this.measureResponseBytes(coalesced),
          });
          return coalesced;
        }
      }
    }

    const limit = query.limit ?? 20;
    const cursorState = this.decodeFeedCursor(query.cursor);
    const cursorOffset = cursorState.offset;
    const rankedAt = cursorState.rankedAt;
    const page = query.page ?? 1;
    const radiusKm = query.radiusKm ?? 30;
    const skip = cursorOffset ?? (page - 1) * limit;
    const requestedPostType = this.resolveRequestedPostType(query.preferredPostType);

    const [currentUser, followingIds, invisibleUserIds, reportedPostIds, notInterestedPostIds, rankingProfile] = await Promise.all([
      this.usersRepository.findById(currentUserId),
      this.feedRepository.findFollowingIds(currentUserId),
      this.blocksService.getInvisibleUserIds(currentUserId),
      this.findReportedPostIds(currentUserId),
      this.findNotInterestedPostIds(currentUserId),
      this.getViewerRankingProfile(currentUserId),
    ]);
    if (!currentUser) {
      throw new NotFoundException('Current user not found');
    }
    const unauthorizedPrivateAuthorIds = await this.usersRepository.findPrivateUserIds([
      currentUserId,
      ...followingIds,
    ]);
    const hiddenAuthorIds = Array.from(
      new Set([...invisibleUserIds, ...unauthorizedPrivateAuthorIds]),
    );
    const relationLookupMs = this.markTiming(timing);
    let filter = this.buildVisiblePostsFilter(
      currentUserId,
      followingIds,
      followingOnly,
      hiddenAuthorIds,
    );
    if (requestedPostType) {
      filter = { $and: [filter, { postType: requestedPostType }] };
    }
    filter = this.addRankingWindowFilter(filter, rankedAt, [...reportedPostIds, ...notInterestedPostIds]);
    const candidateLimit = Math.min(1000, Math.max(limit * 20, 400));
    let candidates = await this.findRankableCandidates(filter, candidateLimit);
    const firstCandidateLookupMs = this.markTiming(timing);

    // Keep the default home feed focused on followed accounts, but avoid an empty screen
    // for new users or when followed accounts have not posted yet.
    if (candidates.length === 0 && typeof query.followingOnly === 'undefined' && followingOnly) {
      filter = this.buildVisiblePostsFilter(currentUserId, followingIds, false, hiddenAuthorIds);
      if (requestedPostType) {
        filter = { $and: [filter, { postType: requestedPostType }] };
      }
      filter = this.addRankingWindowFilter(filter, rankedAt, [...reportedPostIds, ...notInterestedPostIds]);
      candidates = await this.findRankableCandidates(filter, candidateLimit);
    }
    const fallbackCandidateLookupMs = this.markTiming(timing);

    const activeAuthorCandidates = candidates.filter((post) => {
      const author = post.authorId as unknown as { isDisabled?: boolean } | null;
      return !!author && author.isDisabled !== true;
    });
    const filteredCandidates = requestedPostType
      ? activeAuthorCandidates.filter((post) => post.postType === requestedPostType)
      : activeAuthorCandidates;

    const scored = filteredCandidates
      .map((post) => ({
        post,
        score: this.scorePost({
          currentUser,
          currentUserId,
          followingIds,
          post,
          preferredPostType: query.preferredPostType,
          radiusKm,
          rankingProfile,
          rankedAt,
        }),
      }))
      .sort(
        (a, b) =>
          b.score - a.score ||
          new Date((b.post as any).createdAt ?? 0).getTime() -
            new Date((a.post as any).createdAt ?? 0).getTime(),
      );
    const scoringMs = this.markTiming(timing);

    const diversified = this.diversifyRankedPosts(scored);
    const total = diversified.length;
    const pagedPosts = diversified.slice(skip, skip + limit).map((entry) => ({
      ...(entry.post.toObject() as unknown as Record<string, unknown>),
      feedScore: Number(entry.score.toFixed(3)),
    }));
    const decoratedPosts = await this.decoratePostsForViewer(
      currentUserId,
      pagedPosts,
      followingIds,
    );
    const normalizedPosts = this.normalizePreferredPostTypeForResponse(
      decoratedPosts,
      query.preferredPostType,
    );
    const decorationMs = this.markTiming(timing);
    const items = includeSuggestions
      ? await this.mixHomeFeedItems(currentUserId, normalizedPosts, query.suggestionInterval ?? 4)
      : normalizedPosts;
    const cardsMs = this.markTiming(timing);
    const nextOffset = skip + pagedPosts.length;
    const nextCursor = nextOffset < total ? this.encodeFeedCursor(nextOffset, rankedAt) : null;

    const result = buildPaginatedResponse(items, {
      page,
      limit,
      total,
      offset: skip,
      currentCursor: query.cursor ?? null,
      nextCursor,
      mode: 'cursor',
    });
    result.pagination.hasNextPage = nextCursor !== null;
    result.pagination.nextPage = nextCursor !== null ? page + 1 : null;

    if (cacheEnabled) {
      await this.cacheService.set(
        cacheKey,
        result,
        this.configService.get<number>('feedCache.userFeedTtlSeconds', { infer: true }) ?? 15,
      );
    }
    if (cacheFillLock?.acquired) {
      await cacheFillLock.release().catch(() => false);
    }

    this.logFeedTiming(timing, {
      cacheHit: false,
      page,
      limit,
      skip,
      followingOnly,
      includeSuggestions,
      followingCount: followingIds.length,
      invisibleUserCount: invisibleUserIds.length,
      candidateCount: candidates.length,
      filteredCandidateCount: filteredCandidates.length,
      scoredCount: scored.length,
      itemCount: items.length,
      postItemCount: normalizedPosts.length,
      nextCursor,
      relationLookupMs,
      firstCandidateLookupMs,
      fallbackCandidateLookupMs,
      scoringMs,
      decorationMs,
      cardsMs,
      responseBytes: this.measureResponseBytes(result),
    });

    return result;
  }

  private async findRankableCandidates(
    filter: Record<string, unknown>,
    limit: number,
  ): Promise<Array<Record<string, any> & { toObject(): unknown }>> {
    const recentLimit = Math.ceil(limit * 0.65);
    const popularLimit = limit - recentLimit;
    const [recent, popular] = await Promise.all([
      this.feedRepository.findCandidatePosts(filter, recentLimit),
      this.feedRepository.findPopularCandidatePosts(filter, popularLimit),
    ]);
    const unique = new Map<string, Record<string, any> & { toObject(): unknown }>();
    for (const post of [...recent, ...popular]) {
      const id = this.extractEntityId((post as any)._id ?? (post as any).id);
      if (id && !unique.has(id)) {
        unique.set(id, post as unknown as Record<string, any> & { toObject(): unknown });
      }
    }
    return Array.from(unique.values());
  }

  private addRankingWindowFilter(
    filter: Record<string, unknown>,
    rankedAt: Date,
    reportedPostIds: Types.ObjectId[],
  ): Record<string, unknown> {
    const clauses: Record<string, unknown>[] = [filter, { createdAt: { $lte: rankedAt } }];
    if (reportedPostIds.length) {
      clauses.push({ _id: { $nin: reportedPostIds } });
    }
    return { $and: clauses };
  }

  private async findReportedPostIds(currentUserId: string): Promise<Types.ObjectId[]> {
    if (!Types.ObjectId.isValid(currentUserId)) {
      return [];
    }
    const rows = await this.connection
      .collection('reports')
      .find({
        reporterId: new Types.ObjectId(currentUserId),
        targetType: 'post',
        status: { $ne: 'rejected' },
      })
      .project({ targetId: 1 })
      .toArray();
    return rows
      .map((row) => row.targetId)
      .filter((id): id is Types.ObjectId => id instanceof Types.ObjectId);
  }

  private async findNotInterestedPostIds(currentUserId: string): Promise<Types.ObjectId[]> {
    const rows = await this.connection
      .collection<{ postId?: Types.ObjectId }>('postpreferences')
      .find({ userId: new Types.ObjectId(currentUserId), notInterested: true })
      .project({ postId: 1 })
      .toArray();
    return rows.map((row) => row.postId).filter((id): id is Types.ObjectId => id instanceof Types.ObjectId);
  }

  private async getViewerRankingProfile(currentUserId: string): Promise<ViewerRankingProfile> {
    const empty: ViewerRankingProfile = {
      authorAffinity: {},
      postTypeAffinity: {},
      hashtagAffinity: {},
      interactedPostIds: [],
      engagementSignals: {},
    };
    if (!Types.ObjectId.isValid(currentUserId)) {
      return empty;
    }

    const cacheEnabled =
      this.configService.get<boolean>('feedCache.enabled', { infer: true }) ?? true;
    const cacheKey = `feed:ranking-profile:${currentUserId}`;
    if (cacheEnabled) {
      const cached = await this.cacheService.get<ViewerRankingProfile>(cacheKey);
      if (cached) {
        return cached;
      }
    }

    const userId = new Types.ObjectId(currentUserId);
    const cutoff = new Date(Date.now() - 90 * 24 * 60 * 60 * 1000);
    const [likes, saves, shares, comments, engagementRows] = await Promise.all([
      this.connection
        .collection('likes')
        .find({ userId, targetType: 'post', createdAt: { $gte: cutoff } })
        .sort({ createdAt: -1 })
        .limit(250)
        .project({ targetId: 1 })
        .toArray(),
      this.connection
        .collection('saves')
        .find({ userId, createdAt: { $gte: cutoff } })
        .sort({ createdAt: -1 })
        .limit(250)
        .project({ postId: 1 })
        .toArray(),
      this.connection
        .collection('postshares')
        .find({ userId, isDeleted: false, createdAt: { $gte: cutoff } })
        .sort({ createdAt: -1 })
        .limit(250)
        .project({ postId: 1 })
        .toArray(),
      this.connection
        .collection('comments')
        .find({ authorId: userId, isDeleted: { $ne: true }, createdAt: { $gte: cutoff } })
        .sort({ createdAt: -1 })
        .limit(250)
        .project({ postId: 1 })
        .toArray(),
      this.connection.collection('engagementevents').aggregate<{
        _id: Types.ObjectId; watchMs: number; completions: number; rewatches: number; profileOpens: number;
      }>([
        { $match: { userId, occurredAt: { $gte: cutoff } } },
        { $group: {
          _id: '$postId', watchMs: { $sum: '$watchTimeMs' },
          completions: { $sum: { $cond: [{ $eq: ['$type', 'complete'] }, 1, 0] } },
          rewatches: { $sum: { $cond: [{ $eq: ['$type', 'rewatch'] }, 1, 0] } },
          profileOpens: { $sum: { $cond: [{ $eq: ['$type', 'profile_open'] }, 1, 0] } },
        } }, { $limit: 500 },
      ]).toArray(),
    ]);

    const weights = new Map<string, number>();
    const addWeight = (value: unknown, weight: number) => {
      if (value instanceof Types.ObjectId) {
        const id = value.toString();
        weights.set(id, (weights.get(id) ?? 0) + weight);
      }
    };
    likes.forEach((row) => addWeight(row.targetId, 1.5));
    saves.forEach((row) => addWeight(row.postId, 4));
    shares.forEach((row) => addWeight(row.postId, 5));
    comments.forEach((row) => addWeight(row.postId, 3));
    engagementRows.forEach((row) =>
      addWeight(row._id, Math.min(12, row.watchMs / 60_000) + row.completions * 4 + row.rewatches * 5 + row.profileOpens * 2),
    );
    if (!weights.size) {
      return empty;
    }

    const postIds = Array.from(weights.keys()).map((id) => new Types.ObjectId(id));
    const posts = await this.connection
      .collection('posts')
      .find({ _id: { $in: postIds }, isDeleted: { $ne: true } })
      .project({ authorId: 1, postType: 1, hashtags: 1 })
      .toArray();
    const profile: ViewerRankingProfile = {
      authorAffinity: {},
      postTypeAffinity: {},
      hashtagAffinity: {},
      interactedPostIds: Array.from(weights.keys()),
      engagementSignals: Object.fromEntries(engagementRows.map((row) => [row._id.toString(), row])),
    };
    for (const post of posts) {
      const postWeight = weights.get(post._id.toString()) ?? 0;
      const authorId = post.authorId?.toString?.() ?? '';
      const postType = typeof post.postType === 'string' ? post.postType : '';
      if (authorId) {
        profile.authorAffinity[authorId] = (profile.authorAffinity[authorId] ?? 0) + postWeight;
      }
      if (postType) {
        profile.postTypeAffinity[postType] = (profile.postTypeAffinity[postType] ?? 0) + postWeight;
      }
      for (const hashtag of Array.isArray(post.hashtags) ? post.hashtags : []) {
        const normalized = String(hashtag).trim().toLowerCase();
        if (normalized) {
          profile.hashtagAffinity[normalized] =
            (profile.hashtagAffinity[normalized] ?? 0) + postWeight;
        }
      }
    }

    if (cacheEnabled) {
      await this.cacheService.set(
        cacheKey,
        profile,
        this.configService.get<number>('feedCache.rankingProfileTtlSeconds', {
          infer: true,
        }) ?? 30,
      );
    }
    return profile;
  }

  private diversifyRankedPosts(ranked: RankedPost[]): RankedPost[] {
    const remaining = [...ranked];
    const result: RankedPost[] = [];
    const authorCounts = new Map<string, number>();
    const typeCounts = new Map<string, number>();
    const hashtagCounts = new Map<string, number>();

    while (remaining.length) {
      let bestIndex = 0;
      let bestAdjustedScore = Number.NEGATIVE_INFINITY;
      for (let index = 0; index < remaining.length; index += 1) {
        const entry = remaining[index];
        const authorId = this.extractEntityId(entry.post.authorId);
        const postType = String(entry.post.postType ?? '');
        const hashtags = Array.isArray(entry.post.hashtags)
          ? entry.post.hashtags.map((tag: unknown) => String(tag).toLowerCase())
          : [];
        const previousAuthorId = result.length
          ? this.extractEntityId(result[result.length - 1].post.authorId)
          : '';
        const authorPenalty = (authorCounts.get(authorId) ?? 0) * 12;
        const consecutivePenalty = authorId && authorId === previousAuthorId ? 22 : 0;
        const typePenalty = Math.max(0, (typeCounts.get(postType) ?? 0) - 1) * 2.5;
        const hashtagPenalty = Math.min(
          8,
          hashtags.reduce((sum, tag) => sum + (hashtagCounts.get(tag) ?? 0) * 0.75, 0),
        );
        const adjusted =
          entry.score - authorPenalty - consecutivePenalty - typePenalty - hashtagPenalty;
        if (adjusted > bestAdjustedScore) {
          bestAdjustedScore = adjusted;
          bestIndex = index;
        }
      }

      const [selected] = remaining.splice(bestIndex, 1);
      const authorId = this.extractEntityId(selected.post.authorId);
      const postType = String(selected.post.postType ?? '');
      authorCounts.set(authorId, (authorCounts.get(authorId) ?? 0) + 1);
      typeCounts.set(postType, (typeCounts.get(postType) ?? 0) + 1);
      for (const hashtag of Array.isArray(selected.post.hashtags) ? selected.post.hashtags : []) {
        const normalized = String(hashtag).toLowerCase();
        hashtagCounts.set(normalized, (hashtagCounts.get(normalized) ?? 0) + 1);
      }
      result.push({ ...selected, score: bestAdjustedScore });
    }
    return result;
  }

  private decodeFeedCursor(cursor?: string): { offset: number | null; rankedAt: Date } {
    const fallback = { offset: decodeOffsetCursor(cursor), rankedAt: new Date() };
    if (!cursor) {
      return fallback;
    }
    try {
      const payload = JSON.parse(Buffer.from(cursor, 'base64url').toString('utf8')) as {
        offset?: unknown;
        rankedAt?: unknown;
      };
      const rankedAt = new Date(String(payload.rankedAt ?? ''));
      if (
        !Number.isInteger(payload.offset) ||
        Number(payload.offset) < 0 ||
        Number.isNaN(rankedAt.getTime())
      ) {
        return fallback;
      }
      return { offset: Number(payload.offset), rankedAt };
    } catch {
      return fallback;
    }
  }

  private encodeFeedCursor(offset: number, rankedAt: Date): string {
    return Buffer.from(
      JSON.stringify({ version: 1, offset, rankedAt: rankedAt.toISOString() }),
      'utf8',
    ).toString('base64url');
  }

  async getExplore(currentUserId: string, query: FeedQueryDto) {
    return this.getTrending(currentUserId, {
      ...query,
      followingOnly: false,
      includeSuggestions: false,
    });
  }

  async getReels(currentUserId: string, query: FeedQueryDto) {
    return this.getMyFeed(currentUserId, {
      ...query,
      preferredPostType: 'reel',
      includeSuggestions: false,
    });
  }

  async getTrending(currentUserId: string, query: FeedQueryDto) {
    const timing = this.startTiming('feed.trending');
    const cacheEnabled =
      this.configService.get<boolean>('feedCache.enabled', { infer: true }) ?? true;
    const globalVersion = cacheEnabled ? await this.feedVersionService.getGlobalVersion() : 0;
    const cacheKey = this.buildCacheKey('trending', {
      currentUserId,
      globalVersion,
      page: query.page ?? 1,
      limit: query.limit ?? 20,
      cursor: query.cursor ?? '',
      preferredPostType: query.preferredPostType ?? '',
    });
    let cacheFillLock: Awaited<ReturnType<AppCacheService['acquireFillLock']>> | null = null;
    if (cacheEnabled) {
      const cached = await this.cacheService.get<Record<string, unknown>>(cacheKey);
      if (cached) {
        this.logFeedTiming(timing, {
          cacheHit: true,
          itemCount: Array.isArray(cached.items) ? cached.items.length : undefined,
          nextCursor: cached.nextCursor ?? null,
          responseBytes: this.measureResponseBytes(cached),
        });
        return cached;
      }
      cacheFillLock = await this.cacheService.acquireFillLock(cacheKey, 10);
      if (!cacheFillLock.acquired) {
        const coalesced = await this.cacheService.waitForValue<Record<string, unknown>>(
          cacheKey,
          2_000,
        );
        if (coalesced) {
          this.logFeedTiming(timing, {
            cacheHit: true,
            cacheCoalesced: true,
            itemCount: Array.isArray(coalesced.items) ? coalesced.items.length : undefined,
            responseBytes: this.measureResponseBytes(coalesced),
          });
          return coalesced;
        }
      }
    }

    const limit = query.limit ?? 20;
    const cursorOffset = decodeOffsetCursor(query.cursor);
    const page = query.page ?? 1;
    const skip = cursorOffset ?? (page - 1) * limit;
    const [followingIds, invisibleUserIds, privateAuthorIds] = await Promise.all([
      this.feedRepository.findFollowingIds(currentUserId),
      this.blocksService.getInvisibleUserIds(currentUserId),
      this.usersRepository.findPrivateUserIds(),
    ]);
    const relationLookupMs = this.markTiming(timing);
    const trendingFilter: Record<string, unknown> = { visibility: PostVisibility.PUBLIC };
    const excludedAuthorIds = Array.from(new Set([...invisibleUserIds, ...privateAuthorIds]));
    if (excludedAuthorIds.length) {
      trendingFilter.authorId = { $nin: excludedAuthorIds.map((id) => new Types.ObjectId(id)) };
    }
    const requestedPostType = this.resolveRequestedPostType(query.preferredPostType);
    if (requestedPostType) {
      trendingFilter.postType = requestedPostType;
    }

    const [rows, total] = await Promise.all([
      this.feedRepository.findTrendingPublicPosts(trendingFilter, skip, limit),
      this.feedRepository.count(trendingFilter),
    ]);
    const postLookupMs = this.markTiming(timing);
    const visibleRows = rows.filter((post) => {
      const author = post.authorId as unknown as { isDisabled?: boolean } | null;
      return !!author && author.isDisabled !== true;
    });
    const decoratedPosts = await this.decoratePostsForViewer(
      currentUserId,
      visibleRows.map((item) => item.toObject() as unknown as Record<string, unknown>),
      followingIds,
    );
    const normalizedPosts = this.normalizePreferredPostTypeForResponse(
      decoratedPosts,
      query.preferredPostType,
    );
    const decorationMs = this.markTiming(timing);
    const nextOffset = skip + rows.length;
    const nextCursor = nextOffset < total ? encodeOffsetCursor(nextOffset) : null;

    const result = buildPaginatedResponse(normalizedPosts, {
      page,
      limit,
      total,
      offset: skip,
      currentCursor: query.cursor ?? null,
      nextCursor,
      mode: 'cursor',
    });

    if (cacheEnabled) {
      await this.cacheService.set(
        cacheKey,
        result,
        this.configService.get<number>('feedCache.trendingTtlSeconds', { infer: true }) ?? 30,
      );
    }
    if (cacheFillLock?.acquired) {
      await cacheFillLock.release().catch(() => false);
    }

    this.logFeedTiming(timing, {
      cacheHit: false,
      page,
      limit,
      skip,
      preferredPostType: query.preferredPostType ?? '',
      followingCount: followingIds.length,
      invisibleUserCount: invisibleUserIds.length,
      itemCount: normalizedPosts.length,
      total,
      nextCursor,
      relationLookupMs,
      postLookupMs,
      decorationMs,
      responseBytes: this.measureResponseBytes(result),
    });

    return result;
  }

  private async decoratePostsForViewer(
    currentUserId: string,
    items: Array<Record<string, unknown>>,
    followingIds: string[],
  ): Promise<FeedPostItem[]> {
    const safeItems = await this.redactInaccessibleOriginalReferences(
      currentUserId,
      items,
      followingIds,
    );
    const postIds = safeItems.map((item) => this.extractEntityId(item._id ?? item.id)).filter(Boolean);
    const followingSet = new Set(followingIds);
    const [likedPostIds, savedPostIds, sharedPostIds] = await Promise.all([
      this.likesRepository.findLikedPostIds(currentUserId, postIds),
      this.savesRepository.findSavedPostIds(currentUserId, postIds),
      this.findSharedPostIds(currentUserId, postIds),
    ]);
    const likedSet = new Set(likedPostIds);
    const savedSet = new Set(savedPostIds);
    const sharedSet = new Set(sharedPostIds);

    return safeItems.map((item) => {
      const postId = this.extractEntityId(item._id ?? item.id);
      const authorId = this.extractEntityId(item.authorId);
      const likesCount = Number(item.likesCount ?? 0);
      const commentsCount = Number(item.commentsCount ?? 0);
      const savesCount = Number(item.savesCount ?? 0);
      const shareCount = Number(item.shareCount ?? 0);
      const viewCount = Number(item.viewCount ?? 0);
      const playCount = Number(item.playCount ?? 0);

      return {
        ...item,
        id: postId,
        feedItemType: 'post',
        likedByMe: likedSet.has(postId),
        savedByMe: savedSet.has(postId),
        isSharedByMe: sharedSet.has(postId),
        followingAuthor: !!authorId && followingSet.has(authorId),
        isOwnPost: authorId === currentUserId,
        canComment: true,
        canMessage: !!authorId && authorId !== currentUserId,
        engagement: {
          likesCount,
          commentsCount,
          savesCount,
          shareCount,
          viewCount,
          playCount,
        },
      };
    });
  }

  private async redactInaccessibleOriginalReferences(
    currentUserId: string,
    items: Array<Record<string, unknown>>,
    followingIds: string[],
  ): Promise<Array<Record<string, unknown>>> {
    const fields = ['repostOfPostId', 'quoteOfPostId'] as const;
    const sourceIds = Array.from(
      new Set(
        items
          .flatMap((item) => fields.map((field) => this.extractEntityId(item[field])))
          .filter((id) => Types.ObjectId.isValid(id)),
      ),
    );
    if (!sourceIds.length) {
      return items;
    }

    const [sources, unauthorizedPrivateAuthorIds, invisibleUserIds] = await Promise.all([
      this.connection
        .collection('posts')
        .find({
          _id: { $in: sourceIds.map((id) => new Types.ObjectId(id)) },
          isDeleted: { $ne: true },
          isArchived: { $ne: true },
          moderationStatus: { $ne: 'hidden' },
        })
        .project({ authorId: 1, visibility: 1 })
        .toArray(),
      this.usersRepository.findPrivateUserIds([currentUserId, ...followingIds]),
      this.blocksService.getInvisibleUserIds(currentUserId),
    ]);
    const sourceById = new Map(sources.map((source) => [source._id.toString(), source]));
    const followingSet = new Set(followingIds);
    const excludedAuthorSet = new Set([
      ...unauthorizedPrivateAuthorIds,
      ...invisibleUserIds,
    ]);

    return items.map((item) => {
      const safe = { ...item };
      for (const field of fields) {
        const sourceId = this.extractEntityId(item[field]);
        if (!sourceId) continue;
        const source = sourceById.get(sourceId);
        const authorId = this.extractEntityId(source?.authorId);
        const allowed =
          !!source &&
          !!authorId &&
          !excludedAuthorSet.has(authorId) &&
          (authorId === currentUserId ||
            source.visibility === PostVisibility.PUBLIC ||
            (source.visibility === PostVisibility.FOLLOWERS &&
              followingSet.has(authorId)));
        if (!allowed) {
          safe[field] = null;
        }
      }
      return safe;
    });
  }

  private async findSharedPostIds(currentUserId: string, postIds: string[]): Promise<string[]> {
    if (!Types.ObjectId.isValid(currentUserId) || postIds.length === 0) {
      return [];
    }

    const validPostIds = Array.from(new Set(postIds.filter((id) => Types.ObjectId.isValid(id))));
    if (!validPostIds.length) {
      return [];
    }

    const rows = await this.connection
      .collection<{ postId: Types.ObjectId }>('postshares')
      .find({
        userId: new Types.ObjectId(currentUserId),
        postId: { $in: validPostIds.map((id) => new Types.ObjectId(id)) },
        target: PostShareTarget.PROFILE,
        isDeleted: false,
        deletedAt: null,
      })
      .project({ postId: 1 })
      .toArray();

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

  private async mixHomeFeedItems(
    currentUserId: string,
    posts: FeedPostItem[],
    suggestionInterval: number,
  ): Promise<Array<FeedPostItem | FeedCardItem>> {
    const cards = await this.buildHomeCards(currentUserId);
    if (!cards.length) {
      return posts;
    }

    const result: Array<FeedPostItem | FeedCardItem> = [];
    let cardIndex = 0;

    for (let index = 0; index < posts.length; index += 1) {
      result.push(posts[index]);
      if ((index + 1) % suggestionInterval === 0 && cardIndex < cards.length) {
        result.push(cards[cardIndex]);
        cardIndex += 1;
      }
    }

    while (cardIndex < cards.length) {
      result.push(cards[cardIndex]);
      cardIndex += 1;
    }

    return result;
  }

  private async buildHomeCards(currentUserId: string): Promise<FeedCardItem[]> {
    const [suggestions, listings, instruments, repairShops] = await Promise.all([
      this.followsService.getSuggestions(currentUserId, {
        page: 1,
        limit: 5,
      }),
      this.marketplaceService.getPublicListings({
        page: 1,
        limit: 3,
        isActive: true,
      } as any),
      this.marketplaceService.getPublicInstruments({
        page: 1,
        limit: 3,
        isActive: true,
      } as any),
      this.marketplaceService.getPublicRepairShops({
        page: 1,
        limit: 2,
        isActive: true,
      } as any),
    ]);

    const cards: FeedCardItem[] = [];
    if (Array.isArray(suggestions.items) && suggestions.items.length > 0) {
      cards.push({
        id: `suggested-users:${currentUserId}`,
        feedItemType: 'suggested_users',
        title: 'Suggested creators',
        subtitle: 'People you may want to follow',
        items: suggestions.items.map((entry) => ({
          ...entry,
          following: false,
        })),
      });
    }

    if (
      (listings.items?.length ?? 0) > 0 ||
      (instruments.items?.length ?? 0) > 0 ||
      (repairShops.items?.length ?? 0) > 0
    ) {
      cards.push({
        id: `featured-marketplace:${currentUserId}`,
        feedItemType: 'featured_marketplace',
        title: 'Explore marketplace',
        subtitle: 'Featured listings, musical instruments, and repair shops',
        listings: (listings.items ?? []) as unknown as Array<Record<string, unknown>>,
        musicalInstruments: (instruments.items ?? []) as unknown as Array<Record<string, unknown>>,
        instruments: (instruments.items ?? []) as unknown as Array<Record<string, unknown>>,
        repairShops: (repairShops.items ?? []) as unknown as Array<Record<string, unknown>>,
      });
    }

    return cards;
  }

  private buildVisiblePostsFilter(
    currentUserId: string,
    followingIds: string[],
    followingOnly: boolean,
    invisibleUserIds: string[] = [],
  ): Record<string, unknown> {
    const currentUserObjectId = new Types.ObjectId(currentUserId);
    const followingObjectIds = followingIds.map((id) => new Types.ObjectId(id));

    const visibilityFilter = followingOnly
      ? {
          $or: [
            { authorId: currentUserObjectId },
            {
              authorId: { $in: followingObjectIds },
              visibility: { $in: [PostVisibility.PUBLIC, PostVisibility.FOLLOWERS] },
            },
          ],
        }
      : {
          $or: [
            { visibility: PostVisibility.PUBLIC },
            { authorId: currentUserObjectId },
            {
              authorId: { $in: followingObjectIds },
              visibility: PostVisibility.FOLLOWERS,
            },
          ],
        };

    if (!invisibleUserIds.length) {
      return visibilityFilter;
    }

    return {
      $and: [
        visibilityFilter,
        { authorId: { $nin: invisibleUserIds.map((id) => new Types.ObjectId(id)) } },
      ],
    };
  }

  private scorePost(input: {
    currentUser: UserDocument;
    currentUserId: string;
    followingIds: string[];
    post: Record<string, any>;
    preferredPostType?: FeedPostTypeFilter;
    radiusKm: number;
    rankingProfile: ViewerRankingProfile;
    rankedAt: Date;
  }): number {
    const {
      currentUser,
      currentUserId,
      followingIds,
      post,
      preferredPostType,
      radiusKm,
      rankingProfile,
      rankedAt,
    } = input;
    const author = post.authorId;
    const authorId = this.extractEntityId(author);
    const isOwnPost = authorId === currentUserId;
    const isFollowing = followingIds.includes(authorId);

    const ageMs = Math.max(0, rankedAt.getTime() - new Date(post.createdAt).getTime());
    const ageHours = ageMs / (1000 * 60 * 60);
    const freshness = 32 * Math.exp(-ageHours / (24 * 3));
    const veryRecentBoost = ageHours <= 6 ? 5 : 0;

    const engagement =
      Math.log1p(Math.max(0, Number(post.likesCount ?? 0))) * 2.4 +
      Math.log1p(Math.max(0, Number(post.commentsCount ?? 0))) * 3.2 +
      Math.log1p(Math.max(0, Number(post.savesCount ?? 0))) * 4.2 +
      Math.log1p(Math.max(0, Number(post.shareCount ?? 0))) * 4.8 +
      Math.log1p(Math.max(0, Number(post.viewCount ?? 0))) * 0.7 +
      Math.log1p(Math.max(0, Number(post.playCount ?? 0))) * 1.1;
    const normalizedHashtags = (post.hashtags ?? [])
      .map((value: string) => value.trim().toLowerCase())
      .filter(Boolean);
    const hashtagMatches = this.intersectionCount(
      this.buildPreferenceTokens(currentUser),
      normalizedHashtags,
    );
    const behaviorHashtagAffinity = normalizedHashtags.reduce(
      (sum: number, hashtag: string) => sum + (rankingProfile.hashtagAffinity[hashtag] ?? 0),
      0,
    );
    const authorAffinity = rankingProfile.authorAffinity[authorId] ?? 0;
    const postTypeAffinity = rankingProfile.postTypeAffinity[String(post.postType ?? '')] ?? 0;
    const interactedBefore = rankingProfile.interactedPostIds.includes(
      this.extractEntityId(post._id ?? post.id),
    );
    const behavior = (rankingProfile.engagementSignals ?? {})[this.extractEntityId(post._id ?? post.id)];
    const watchQualityBoost = behavior
      ? Math.min(18, Math.log1p(Math.max(0, behavior.watchMs) / 1000) * 2) +
        Math.min(12, behavior.completions * 4) + Math.min(12, behavior.rewatches * 6) +
        Math.min(6, behavior.profileOpens * 2)
      : 0;

    const distanceKm = this.computeDistanceKm(
      currentUser.latitude,
      currentUser.longitude,
      author?.latitude ?? null,
      author?.longitude ?? null,
    );
    const nearbyBoost =
      typeof distanceKm === 'number' && distanceKm <= radiusKm
        ? Math.max(0, 8 - (distanceKm / radiusKm) * 8)
        : 0;

    let score = 0;
    score += engagement;
    score += freshness;
    score += veryRecentBoost;
    score += isOwnPost ? 3 : 0;
    score += isFollowing ? 24 : 0;
    score += post.postType === preferredPostType ? 12 : 0;
    score += hashtagMatches * 7;
    score += Math.min(24, Math.log1p(authorAffinity) * 7);
    score += Math.min(14, Math.log1p(postTypeAffinity) * 4);
    score += Math.min(16, Math.log1p(behaviorHashtagAffinity) * 4);
    score += watchQualityBoost;
    score += nearbyBoost;
    score += author?.isVerified ? 2.5 : 0;
    score += Math.min(6, Math.log1p(Math.max(0, Number(author?.followersCount ?? 0))));
    score -= interactedBefore ? 10 : 0;

    return score;
  }

  private buildPreferenceTokens(user: UserDocument): string[] {
    const tokens = [
      ...(user.musicGenres ?? []),
      ...(user.favoriteInstruments ?? []),
      ...(user.favoriteMaqamat ?? []),
      ...(user.musicRoles ?? []),
    ]
      .map((item) => item.trim().toLowerCase())
      .filter(Boolean);

    return Array.from(new Set(tokens));
  }

  private intersectionCount(a: string[], b: string[]): number {
    if (!a.length || !b.length) {
      return 0;
    }

    const right = new Set(b);
    let count = 0;
    for (const item of a) {
      if (right.has(item)) {
        count += 1;
      }
    }
    return count;
  }

  private computeDistanceKm(
    lat1: number | null | undefined,
    lon1: number | null | undefined,
    lat2: number | null | undefined,
    lon2: number | null | undefined,
  ): number | null {
    if (
      typeof lat1 !== 'number' ||
      typeof lon1 !== 'number' ||
      typeof lat2 !== 'number' ||
      typeof lon2 !== 'number'
    ) {
      return null;
    }

    const toRad = (deg: number) => (deg * Math.PI) / 180;
    const earthKm = 6371;
    const dLat = toRad(lat2 - lat1);
    const dLon = toRad(lon2 - lon1);
    const a =
      Math.sin(dLat / 2) ** 2 +
      Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) * Math.sin(dLon / 2) ** 2;
    const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
    return earthKm * c;
  }

  private buildCacheKey(scope: string, input: Record<string, unknown>): string {
    return `feed:${scope}:${JSON.stringify(input)}`;
  }

  private shouldIncludeSuggestions(query: FeedQueryDto): boolean {
    return (
      query.includeSuggestions === true && !(query.cursor ?? '').trim() && (query.page ?? 1) === 1
    );
  }

  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 resolveRequestedPostType(preferredPostType?: FeedPostTypeFilter): PostType | null {
    if (!preferredPostType) {
      return null;
    }

    return preferredPostType === 'reel' ? PostType.VIDEO : preferredPostType;
  }

  private normalizePreferredPostTypeForResponse<T extends FeedPostItem>(
    items: T[],
    preferredPostType?: FeedPostTypeFilter,
  ): T[] {
    if (preferredPostType !== 'reel') {
      return items;
    }

    return items.map((item) => ({
      ...item,
      postType: 'reel',
    }));
  }

  private startTiming(scope: string): { scope: string; startedAt: number; lastAt: number } {
    const now = performance.now();
    return { scope, startedAt: now, lastAt: now };
  }

  private markTiming(timing: { lastAt: number }): number {
    const now = performance.now();
    const elapsed = now - timing.lastAt;
    timing.lastAt = now;
    return Math.round(elapsed);
  }

  private logFeedTiming(
    timing: { scope: string; startedAt: number },
    details: Record<string, unknown>,
  ): void {
    if (!this.configService.get<boolean>('performance.feedTimingLogsEnabled', { infer: true })) {
      return;
    }

    this.logger.log({
      event: 'feed_timing',
      scope: timing.scope,
      totalMs: Math.round(performance.now() - timing.startedAt),
      ...details,
    });
  }

  private measureResponseBytes(value: unknown): number {
    try {
      return Buffer.byteLength(JSON.stringify(value), 'utf8');
    } catch {
      return 0;
    }
  }
}
