import { Injectable, Logger, NotFoundException } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { 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 { SavesRepository } from '../saves/saves.repository';
import { UserDocument } from '../users/schemas/user.schema';
import { UsersRepository } from '../users/users.repository';
import { FeedQueryDto } from './dto/feed-query.dto';
import { FeedRepository } from './feed.repository';

type FeedPostItem = Record<string, unknown> & {
  feedItemType: 'post';
  feedScore?: number;
  likedByMe: boolean;
  savedByMe: 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>>;
    };

@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,
  ) {}

  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 = cacheEnabled ? await this.feedVersionService.getGlobalVersion() : 0;
    const includeSuggestions = this.shouldIncludeSuggestions(query);
    const cacheKey = this.buildCacheKey('me', {
      currentUserId,
      globalVersion,
      page: query.page ?? 1,
      limit: query.limit ?? 20,
      cursor: query.cursor ?? '',
      followingOnly,
      radiusKm: query.radiusKm ?? 30,
      preferredPostType: query.preferredPostType ?? '',
      includeSuggestions,
      suggestionInterval: query.suggestionInterval ?? 4,
    });
    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;
      }
    }

    const currentUser = await this.usersRepository.findById(currentUserId);
    if (!currentUser) {
      throw new NotFoundException('Current user not found');
    }

    const limit = query.limit ?? 20;
    const cursorOffset = decodeOffsetCursor(query.cursor);
    const page = query.page ?? 1;
    const radiusKm = query.radiusKm ?? 30;
    const skip = cursorOffset ?? (page - 1) * limit;

    const [followingIds, invisibleUserIds] = await Promise.all([
      this.feedRepository.findFollowingIds(currentUserId),
      this.blocksService.getInvisibleUserIds(currentUserId),
    ]);
    const relationLookupMs = this.markTiming(timing);
    let filter = this.buildVisiblePostsFilter(currentUserId, followingIds, followingOnly, invisibleUserIds);
    let candidates = await this.feedRepository.findCandidatePosts(filter, Math.max(limit * 12, 300));
    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, invisibleUserIds);
      candidates = await this.feedRepository.findCandidatePosts(filter, Math.max(limit * 12, 300));
    }
    const fallbackCandidateLookupMs = this.markTiming(timing);

    const scored = candidates
      .filter((post) => {
        if (!query.preferredPostType) {
          return true;
        }
        return post.postType === query.preferredPostType;
      })
      .map((post) => ({
        post,
        score: this.scorePost({
          currentUser,
          currentUserId,
          followingIds,
          post,
          preferredPostType: query.preferredPostType,
          radiusKm,
        }),
      }))
      .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 total = scored.length;
    const pagedPosts = scored.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 decorationMs = this.markTiming(timing);
    const items = includeSuggestions
      ? await this.mixHomeFeedItems(currentUserId, decoratedPosts, query.suggestionInterval ?? 4)
      : decoratedPosts;
    const cardsMs = this.markTiming(timing);
    const nextOffset = skip + pagedPosts.length;
    const nextCursor = nextOffset < total ? encodeOffsetCursor(nextOffset) : null;

    const result = buildPaginatedResponse(items, {
      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.userFeedTtlSeconds', { infer: true }) ?? 15,
      );
    }

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

    return result;
  }

  async getExplore(currentUserId: string, query: FeedQueryDto) {
    return this.getTrending(currentUserId, {
      ...query,
      followingOnly: false,
      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 ?? '',
    });
    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;
      }
    }

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

    const [rows, total] = await Promise.all([
      this.feedRepository.findTrendingPublicPosts(trendingFilter, skip, limit),
      this.feedRepository.count(trendingFilter),
    ]);
    const postLookupMs = this.markTiming(timing);
    const decoratedPosts = await this.decoratePostsForViewer(
      currentUserId,
      rows.map((item) => item.toObject() as unknown as Record<string, unknown>),
      followingIds,
    );
    const decorationMs = this.markTiming(timing);
    const nextOffset = skip + rows.length;
    const nextCursor = nextOffset < total ? encodeOffsetCursor(nextOffset) : null;

    const result = buildPaginatedResponse(decoratedPosts, {
      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,
      );
    }

    this.logFeedTiming(timing, {
      cacheHit: false,
      page,
      limit,
      skip,
      preferredPostType: query.preferredPostType ?? '',
      followingCount: followingIds.length,
      invisibleUserCount: invisibleUserIds.length,
      itemCount: decoratedPosts.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 postIds = items
      .map((item) => this.extractEntityId(item._id ?? item.id))
      .filter(Boolean);
    const followingSet = new Set(followingIds);
    const [likedPostIds, savedPostIds] = await Promise.all([
      this.likesRepository.findLikedPostIds(currentUserId, postIds),
      this.savesRepository.findSavedPostIds(currentUserId, postIds),
    ]);
    const likedSet = new Set(likedPostIds);
    const savedSet = new Set(savedPostIds);

    return items.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),
        followingAuthor: !!authorId && followingSet.has(authorId),
        isOwnPost: authorId === currentUserId,
        canComment: true,
        canMessage: !!authorId && authorId !== currentUserId,
        engagement: {
          likesCount,
          commentsCount,
          savesCount,
          shareCount,
          viewCount,
          playCount,
        },
      };
    });
  }

  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?: PostType;
    radiusKm: number;
  }): number {
    const { currentUser, currentUserId, followingIds, post, preferredPostType, radiusKm } = input;
    const author = post.authorId;
    const authorId = this.extractEntityId(author);
    const isOwnPost = authorId === currentUserId;
    const isFollowing = followingIds.includes(authorId);

    const ageMs = Date.now() - new Date(post.createdAt).getTime();
    const ageHours = ageMs / (1000 * 60 * 60);
    const freshness = Math.max(0, 36 - ageHours);

    const engagement =
      Number(post.likesCount ?? 0) * 3 +
      Number(post.commentsCount ?? 0) * 4 +
      Number(post.savesCount ?? 0) * 5 +
      Number(post.shareCount ?? 0) * 6 +
      Number(post.viewCount ?? 0) * 0.15 +
      Number(post.playCount ?? 0) * 0.25;
    const hashtagMatches = this.intersectionCount(
      this.buildPreferenceTokens(currentUser),
      (post.hashtags ?? []).map((value: string) => value.toLowerCase()),
    );

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

    let score = 0;
    score += engagement;
    score += freshness;
    score += isOwnPost ? 10 : 0;
    score += isFollowing ? 40 : 0;
    score += post.postType === preferredPostType ? 18 : 0;
    score += hashtagMatches * 9;
    score += nearbyBoost;
    score += author?.isVerified ? 8 : 0;
    score += Math.min(20, Math.floor((author?.followersCount ?? 0) / 200));

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