import { NextResponse } from 'next/server';
import { db, posts, users, media, follows, mutes, blocks, likes, remoteFollows, remotePosts, userSwarmReposts, notifications } from '@/db';
import { getSession, requireAuth } from '@/lib/auth';
import { requireSignedAction, type SignedAction } from '@/lib/auth/verify-signature';
import { eq, desc, and, inArray, isNull, isNotNull, or, lt, sql } from 'drizzle-orm';
import { z } from 'zod';
import { buildNotificationTarget } from '@/lib/notifications';
import { serializeLinkPreviewMedia, parseLinkPreviewMediaJson } from '@/lib/media/linkPreview';
import { shouldIncludeNsfwFeed } from '@/lib/nsfw/feed-access';
import { isLocalNodeNsfw } from '@/lib/node/local-node';
import { hasPublishablePostContent } from '@/lib/posts/content-policy';
import { decodeFeedCursor, encodeFeedCursor } from '@/lib/posts/feed-pagination';
import { mapSwarmPostToPost } from '@/lib/swarm/feed-post';
import {
    CURATED_FEED_WEIGHTS,
    CURATED_FEED_WINDOW_HOURS,
    rankCuratedFeed,
} from '@/lib/posts/curated-feed';
import { registerPostMentions } from '@/lib/mentions/delivery';

const POST_MAX_LENGTH = 600;
const CURATION_SEED_MULTIPLIER = 5;
const CURATION_SEED_CAP = 200;

type FeedPostWithChildren = {
    id: string;
    repostOf?: FeedPostWithChildren | null;
    replyTo?: FeedPostWithChildren | null;
    isLiked?: boolean;
    isReposted?: boolean;
    nodeDomain?: string | null;
    originalPostId?: string | null;
};

function mapUserSwarmRepostToFeedPost(
    row: typeof userSwarmReposts.$inferSelect,
    author: Pick<typeof users.$inferSelect, 'id' | 'handle' | 'displayName' | 'avatarUrl' | 'isBot'>
): FeedPostWithChildren {
    const remoteAuthorHandle = row.authorHandle.includes('@')
        ? row.authorHandle
        : `${row.authorHandle}@${row.nodeDomain}`;
    const remoteOriginalId = `swarm:${row.nodeDomain}:${row.originalPostId}`;

    return {
        id: `swarm-repost:${row.id}`,
        content: '',
        createdAt: row.repostedAt.toISOString(),
        likesCount: 0,
        repostsCount: 0,
        repliesCount: 0,
        author: {
            id: author.id,
            handle: author.handle,
            displayName: author.displayName,
            avatarUrl: author.avatarUrl,
            isBot: author.isBot,
        },
        repostOfId: remoteOriginalId,
        repostOf: {
            id: remoteOriginalId,
            originalPostId: row.originalPostId,
            content: row.content,
            createdAt: row.postCreatedAt.toISOString(),
            likesCount: row.likesCount,
            repostsCount: row.repostsCount,
            repliesCount: row.repliesCount,
            isSwarm: true,
            nodeDomain: row.nodeDomain,
            author: {
                id: `swarm:${row.nodeDomain}:${row.authorHandle}`,
                handle: remoteAuthorHandle,
                displayName: row.authorDisplayName || row.authorHandle,
                avatarUrl: row.authorAvatarUrl,
                isRemote: true,
                nodeDomain: row.nodeDomain,
            },
            media: row.mediaJson ? JSON.parse(row.mediaJson) : [],
            linkPreviewUrl: row.linkPreviewUrl,
            linkPreviewTitle: row.linkPreviewTitle,
            linkPreviewDescription: row.linkPreviewDescription,
            linkPreviewImage: row.linkPreviewImage,
            linkPreviewType: row.linkPreviewType,
            linkPreviewVideoUrl: row.linkPreviewVideoUrl,
            linkPreviewMedia: parseLinkPreviewMediaJson(row.linkPreviewMediaJson) || null,
        },
    } as any;
}

async function getMixedFeedCursorDate(cursor: string | null) {
    if (!cursor) {
        return null;
    }

    const timestampCursor = decodeFeedCursor(cursor);
    if (timestampCursor) {
        return timestampCursor;
    }

    if (cursor.startsWith('swarm-repost:')) {
        const repostRow = await db.query.userSwarmReposts.findFirst({
            where: { id: cursor.replace('swarm-repost:', '') },
        });
        return repostRow?.repostedAt ?? null;
    }

    const cursorPost = await db.query.posts.findFirst({
        where: { id: cursor },
    });
    return cursorPost?.createdAt ?? null;
}

function collectNestedPosts(posts: FeedPostWithChildren[]): FeedPostWithChildren[] {
    const collected: FeedPostWithChildren[] = [];
    const seen = new Set<string>();

    const visit = (post: FeedPostWithChildren | null | undefined) => {
        if (!post || seen.has(post.id)) return;
        seen.add(post.id);
        collected.push(post);
        visit(post.repostOf);
        visit(post.replyTo);
    };

    posts.forEach(visit);
    return collected;
}

function applyInteractionFlags(
    posts: FeedPostWithChildren[],
    likedIds: Set<string>,
    repostedIds: Set<string>
): FeedPostWithChildren[] {
    return posts.map((post) => ({
        ...post,
        isLiked: likedIds.has(post.id),
        isReposted: repostedIds.has(post.id),
        repostOf: post.repostOf ? applyInteractionFlags([post.repostOf], likedIds, repostedIds)[0] : post.repostOf,
        replyTo: post.replyTo ? applyInteractionFlags([post.replyTo], likedIds, repostedIds)[0] : post.replyTo,
    }));
}

const embeddedPostRelations = {
    author: true,
    bot: true,
    media: true,
    replyTo: {
        with: {
            author: true,
            bot: true,
            media: true,
        },
    },
} as const;

const feedPostRelations = {
    ...embeddedPostRelations,
    repostOf: {
        with: embeddedPostRelations,
    },
} as const;

const createPostSchema = z.object({
    content: z.string().max(POST_MAX_LENGTH),
    replyToId: z.string().uuid().optional(), // Must be UUID (swarm replies use separate field)
    swarmReplyTo: z.object({
        postId: z.string(),
        nodeDomain: z.string(),
        content: z.string().optional(),
        author: z.object({
            handle: z.string(),
            displayName: z.string().optional().nullable(),
            avatarUrl: z.string().optional().nullable(),
            nodeDomain: z.string().optional().nullable(),
        }).optional(),
    }).optional(),
    mediaIds: z.array(z.string().uuid()).max(4).optional(),
    isNsfw: z.boolean().optional(),
    linkPreview: z.object({
        url: z.string().url(),
        title: z.string().optional(),
        description: z.string().optional(),
        image: z.string().url().optional().nullable(),
        type: z.enum(['card', 'image', 'gallery', 'video']).optional().nullable(),
        videoUrl: z.string().url().optional().nullable(),
        media: z.array(z.object({
            url: z.string().url(),
            width: z.number().optional().nullable(),
            height: z.number().optional().nullable(),
            mimeType: z.string().optional().nullable(),
        })).optional().nullable(),
    }).optional().nullable(),
}).superRefine((post, context) => {
    if (!hasPublishablePostContent(post.content, post.mediaIds)) {
        context.addIssue({
            code: 'custom',
            path: ['content'],
            message: 'Add text or attach media before posting',
        });
    }
});

function isSignedActionPayload(payload: unknown): payload is SignedAction {
    if (!payload || typeof payload !== 'object') return false;
    const value = payload as Record<string, unknown>;
    return typeof value.action === 'string'
        && typeof value.did === 'string'
        && typeof value.handle === 'string'
        && typeof value.ts === 'number'
        && typeof value.nonce === 'string'
        && typeof value.sig === 'string'
        && typeof value.data === 'object'
        && value.data !== null;
}

// Create a new post
export async function POST(request: Request) {
    try {
        const requestBody = await request.json();
        const user = isSignedActionPayload(requestBody)
            ? await requireSignedAction(requestBody)
            : await requireAuth();
        const data = createPostSchema.parse(
            isSignedActionPayload(requestBody) ? requestBody.data : requestBody
        );

        if (user.isSuspended || user.isSilenced) {
            return NextResponse.json({ error: 'Account restricted' }, { status: 403 });
        }

        const nodeDomain = process.env.NEXT_PUBLIC_NODE_DOMAIN || 'localhost:43821';

        // Build swarm reply fields if replying to a swarm post
        const swarmReplyFields = data.swarmReplyTo ? {
            swarmReplyToId: `swarm:${data.swarmReplyTo.nodeDomain}:${data.swarmReplyTo.postId}`,
            swarmReplyToContent: data.swarmReplyTo.content?.slice(0, 300) || null,
            swarmReplyToAuthor: data.swarmReplyTo.author ? JSON.stringify({
                handle: data.swarmReplyTo.author.handle,
                displayName: data.swarmReplyTo.author.displayName,
                avatarUrl: data.swarmReplyTo.author.avatarUrl,
                nodeDomain: data.swarmReplyTo.nodeDomain,
            }) : null,
        } : {};

        const requestedMediaIds = [...new Set(data.mediaIds || [])];
        let unattachedMedia: typeof media.$inferSelect[] = [];
        if (requestedMediaIds.length > 0) {
            unattachedMedia = await db.query.media.findMany({
                where: { AND: [{ id: { in: requestedMediaIds } }, { userId: user.id }, { postId: { isNull: true } }] },
            });
        }

        if (unattachedMedia.length !== requestedMediaIds.length) {
            return NextResponse.json(
                { error: 'One or more media attachments are unavailable. Please upload them again.' },
                { status: 400 },
            );
        }

        const postContent = data.content.trim();
        if (!hasPublishablePostContent(postContent, unattachedMedia.map(item => item.id))) {
            return NextResponse.json(
                { error: 'Add text or attach media before posting.' },
                { status: 400 },
            );
        }

        const [post] = await db.insert(posts).values({
            userId: user.id,
            content: postContent,
            replyToId: data.replyToId,
            ...swarmReplyFields,
            isNsfw: data.isNsfw || user.isNsfw || false, // Inherit from account if account is NSFW
            apId: `https://${nodeDomain}/posts/${crypto.randomUUID()}`,
            apUrl: `https://${nodeDomain}/posts/${crypto.randomUUID()}`,
            linkPreviewUrl: data.linkPreview?.url,
            linkPreviewTitle: data.linkPreview?.title,
            linkPreviewDescription: data.linkPreview?.description,
            linkPreviewImage: data.linkPreview?.image,
            linkPreviewType: data.linkPreview?.type,
            linkPreviewVideoUrl: data.linkPreview?.videoUrl,
            linkPreviewMediaJson: serializeLinkPreviewMedia(data.linkPreview?.media),
        }).returning();

        try {
            if (data.swarmReplyTo) {
                const protocol = data.swarmReplyTo.nodeDomain.includes('localhost') ? 'http' : 'https';
                const targetUrl = `${protocol}://${data.swarmReplyTo.nodeDomain}/api/swarm/replies`;

                const replyPayload = {
                    postId: data.swarmReplyTo.postId,
                    reply: {
                        id: post.id,
                        content: post.content,
                        createdAt: post.createdAt.toISOString(),
                        author: {
                            handle: user.handle,
                            displayName: user.displayName || user.handle,
                            avatarUrl: user.avatarUrl || undefined,
                            did: user.did,
                            publicKey: user.publicKey,
                        },
                        nodeDomain,
                        mediaUrls: unattachedMedia.map(m => m.url),
                    },
                };

                const { createSignedPayload } = await import('@/lib/swarm/signature');
                const { payload, signature } = await createSignedPayload(replyPayload);

                const response = await fetch(targetUrl, {
                    method: 'POST',
                    headers: {
                        'Content-Type': 'application/json',
                        'X-Swarm-Source-Domain': nodeDomain,
                        'X-Swarm-Signature': signature,
                    },
                    body: JSON.stringify(payload),
                    signal: AbortSignal.timeout(10000),
                });

                if (!response.ok) {
                    const body = await response.text().catch(() => '');
                    throw new Error(body || `Remote node rejected reply (${response.status})`);
                }
            }

            if (requestedMediaIds.length > 0) {
                await db.update(media)
                    .set({ postId: post.id })
                    .where(and(
                        inArray(media.id, requestedMediaIds),
                        eq(media.userId, user.id),
                        isNull(media.postId),
                    ));
            }

            // Update user's post count (atomic increment to prevent race conditions)
            await db.update(users)
                .set({ postsCount: sql`${users.postsCount} + 1` })
                .where(eq(users.id, user.id));

            // If this is a reply, update the parent's reply count (atomic increment)
            if (data.replyToId) {
                await db.update(posts)
                    .set({ repliesCount: sql`${posts.repliesCount} + 1` })
                    .where(eq(posts.id, data.replyToId));
            }
        } catch (err) {
            await db.delete(posts).where(eq(posts.id, post.id));
            console.error('[Swarm] Error creating synchronized reply:', err);
            return NextResponse.json(
                { error: err instanceof Error ? err.message : 'Failed to deliver reply to origin node' },
                { status: 502 }
            );
        }

        let attachedMedia: typeof media.$inferSelect[] = [];
        if (requestedMediaIds.length > 0) {
            attachedMedia = await db.query.media.findMany({
                where: { AND: [{ id: { in: requestedMediaIds } }, { userId: user.id }, { postId: post.id }] },
            });
        }

        if (data.replyToId) {
            try {
                const parentPost = await db.query.posts.findFirst({
                    where: { id: data.replyToId },
                    with: {
                        author: true,
                    },
                });

                if (parentPost && parentPost.userId !== user.id) {
                    const parentAuthor = parentPost.author as typeof users.$inferSelect | undefined;

                    await db.insert(notifications).values({
                        userId: parentPost.userId,
                        actorId: user.id,
                        actorHandle: user.handle,
                        actorDisplayName: user.displayName,
                        actorAvatarUrl: user.avatarUrl,
                        actorNodeDomain: null,
                        postId: parentPost.id,
                        postContent: post.content?.slice(0, 200) || null,
                        ...(parentAuthor?.isBot ? buildNotificationTarget(parentAuthor) : {}),
                        type: 'reply',
                    });

                    if (parentAuthor?.isBot && parentAuthor.botOwnerId) {
                        await db.insert(notifications).values({
                            userId: parentAuthor.botOwnerId,
                            actorId: user.id,
                            actorHandle: user.handle,
                            actorDisplayName: user.displayName,
                            actorAvatarUrl: user.avatarUrl,
                            actorNodeDomain: null,
                            postId: parentPost.id,
                            postContent: post.content?.slice(0, 200) || null,
                            ...buildNotificationTarget(parentAuthor),
                            type: 'reply',
                        });
                    }
                }
            } catch (err) {
                console.error('[Posts] Error creating reply notifications:', err);
                console.error('[Posts] Context:', { postId: post.id, replyToId: data.replyToId, userId: user.id });
            }
        }

        // Resolve local mentions and durably enqueue federated delivery before
        // returning. Remote network I/O is retried from the persistent outbox.
        try {
            await registerPostMentions({
                postId: post.id,
                content: postContent,
                actor: {
                    id: user.id,
                    handle: user.handle,
                    displayName: user.displayName,
                    avatarUrl: user.avatarUrl,
                    did: user.did,
                    publicKey: user.publicKey,
                },
                nodeDomain,
            });
        } catch (err) {
            console.error('[Posts] Error registering mentions:', err);
            console.error('[Posts] Context:', { postId: post.id, userId: user.id, content: postContent.slice(0, 100) });
        }

        // Federate the post to remote followers (non-blocking)
        (async () => {
            try {
                // SWARM-FIRST: Deliver to swarm followers directly
                const { deliverPostToSwarmFollowers } = await import('@/lib/swarm/interactions');

                const swarmResult = await deliverPostToSwarmFollowers(
                    user.id,
                    post,
                    {
                        handle: user.handle,
                        displayName: user.displayName,
                        avatarUrl: user.avatarUrl,
                        isNsfw: user.isNsfw,
                    },
                    attachedMedia,
                    nodeDomain
                );

                if (swarmResult.delivered > 0) {
                    console.log(`[Swarm] Post ${post.id} delivered to ${swarmResult.delivered} swarm nodes (${swarmResult.failed} failed)`);
                }
            } catch (err) {
                // Log error with context but don't fail the request - swarm delivery is best-effort
                console.error('[Posts] Error delivering post to swarm followers:', err);
                console.error('[Posts] Context:', { postId: post.id, userId: user.id, nodeDomain });
            }
        })();


        return NextResponse.json({ success: true, post: { ...post, media: attachedMedia } });
    } catch (error) {
        console.error('Create post error:', error);

        if (error instanceof z.ZodError) {
            return NextResponse.json(
                { error: 'Invalid input', details: error.issues },
                { status: 400 }
            );
        }

        if (error instanceof Error) {
            // Handle signature verification errors
            if (error.message === 'Invalid signature' ||
                error.message === 'User not found' ||
                error.message === 'Handle mismatch' ||
                error.message === 'Timestamp too old or in future') {
                return NextResponse.json(
                    { error: error.message, code: 'INVALID_SIGNATURE' },
                    { status: 403 }
                );
            }

            if (error.message === 'Authentication required') {
                return NextResponse.json({ error: 'Authentication required' }, { status: 401 });
            }
        }

        return NextResponse.json(
            { error: 'Failed to create post' },
            { status: 500 }
        );
    }
}

// Normalize content for deduplication (strip HTML entities, URLs, whitespace, category suffixes)
const normalizeForDedup = (content: string): string => {
    return content
        .replace(/Posted into [\w\s-]+/gi, '') // Remove "Posted into [Category]" patterns
        .replace(/&[a-z]+;/gi, '') // Remove HTML entities like &lsquo;
        .replace(/&#\d+;/g, '') // Remove numeric entities
        .replace(/https?:\/\/[^\s]+/gi, '') // Remove URLs
        .replace(/[^\w\s]/g, '') // Remove punctuation
        .replace(/\s+/g, ' ') // Normalize whitespace
        .toLowerCase()
        .trim()
        .slice(0, 50); // Compare first 50 chars (article title)
};

// Helper to transform cached remote posts to match local post format
// Deduplicates by apId AND by similar content from same author
const transformRemotePosts = (remotePostsData: typeof remotePosts.$inferSelect[]) => {
    const seenApIds = new Set<string>();
    const seenContentKeys = new Set<string>(); // author+normalizedContent
    const uniquePosts: typeof remotePosts.$inferSelect[] = [];

    for (const rp of remotePostsData) {
        if (seenApIds.has(rp.apId)) continue;

        // Content-based dedup: same author + similar content = skip
        const contentKey = `${rp.authorHandle}:${normalizeForDedup(rp.content)}`;
        if (seenContentKeys.has(contentKey)) continue;

        seenApIds.add(rp.apId);
        seenContentKeys.add(contentKey);
        uniquePosts.push(rp);
    }

    return uniquePosts.map(rp => {
        const mediaData = rp.mediaJson ? JSON.parse(rp.mediaJson) : [];
        return {
            id: rp.id,
            content: rp.content,
            createdAt: rp.publishedAt,
            likesCount: 0,
            repostsCount: 0,
            repliesCount: 0,
            isRemote: true,
            apId: rp.apId,
            linkPreviewUrl: rp.linkPreviewUrl,
            linkPreviewTitle: rp.linkPreviewTitle,
            linkPreviewDescription: rp.linkPreviewDescription,
            linkPreviewImage: rp.linkPreviewImage,
            linkPreviewType: rp.linkPreviewType,
            linkPreviewVideoUrl: rp.linkPreviewVideoUrl,
            linkPreviewMedia: parseLinkPreviewMediaJson(rp.linkPreviewMediaJson) || null,
            author: {
                id: rp.authorActorUrl,
                handle: rp.authorHandle,
                displayName: rp.authorDisplayName,
                avatarUrl: rp.authorAvatarUrl,
                isRemote: true,
            },
            media: mediaData.map((m: { url: string; altText?: string }, idx: number) => ({
                id: `${rp.id}-media-${idx}`,
                url: m.url,
                altText: m.altText || null,
            })),
            replyTo: null,
        };
    });
};

// Get timeline / feed
export async function GET(request: Request) {
    try {
        // Return empty posts if no database is connected (for UI testing)
        if (!db) {
            return NextResponse.json({ posts: [], nextCursor: null });
        }

        const { searchParams } = new URL(request.url);
        const type = searchParams.get('type') || 'home'; // home, public, user, curated, replies
        const userId = searchParams.get('userId');
        const cursor = searchParams.get('cursor');
        const limit = Math.min(parseInt(searchParams.get('limit') || '20'), 50);

        let feedPosts;
        // Base filter excludes removed posts and replies (replies only show on detail/profile)
        const baseFilter = {
            isRemoved: false,
            replyToId: { isNull: true as const },
            swarmReplyToId: { isNull: true as const },
        };
        // Filter for replies only
        const repliesFilter = {
            isRemoved: false,
            OR: [
                { replyToId: { isNotNull: true as const } },
                { swarmReplyToId: { isNotNull: true as const } },
            ],
        };

        if (type === 'local') {
            // Local node posts only
            let whereCondition = {
                ...baseFilter,
                createdAt: undefined as { lt: Date } | undefined,
            };

            // Apply cursor-based pagination
            if (cursor) {
                const cursorPost = await db.query.posts.findFirst({
                    where: { id: cursor },
                });
                if (cursorPost) {
                    whereCondition = { ...baseFilter, createdAt: { lt: cursorPost.createdAt } };
                }
            }

            feedPosts = await db.query.posts.findMany({
                where: whereCondition,
                with: feedPostRelations,
                orderBy: (posts, { desc }) => [desc(posts.createdAt)],
                limit,
            });
        } else if (type === 'public') {
            // Public timeline - all local posts + all cached remote posts
            const localPosts = await db.query.posts.findMany({
                where: baseFilter,
                with: feedPostRelations,
                orderBy: (posts, { desc }) => [desc(posts.createdAt)],
                limit: limit * 2,
            });

            // Get all cached remote posts
            const remotePostsData = await db.query.remotePosts.findMany({
                orderBy: (remotePosts, { desc }) => [desc(remotePosts.publishedAt)],
                limit: limit,
            });

            const transformedRemote = transformRemotePosts(remotePostsData);

            // Merge and sort by date
            feedPosts = [...localPosts, ...transformedRemote]
                .sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime())
                .slice(0, limit) as any;
        } else if (type === 'user' && userId) {
            // User's posts (excluding replies)
            let whereCondition = {
                ...baseFilter,
                userId,
                createdAt: undefined as { lt: Date } | undefined,
            };

            // Apply cursor-based pagination
            if (cursor) {
                const cursorPost = await db.query.posts.findFirst({
                    where: { id: cursor },
                });
                if (cursorPost) {
                    whereCondition = { ...baseFilter, userId, createdAt: { lt: cursorPost.createdAt } };
                }
            }

            feedPosts = await db.query.posts.findMany({
                where: whereCondition,
                with: feedPostRelations,
                orderBy: (posts, { desc }) => [desc(posts.createdAt)],
                limit,
            });
        } else if (type === 'replies' && userId) {
            // User's replies only
            let whereCondition = {
                ...repliesFilter,
                userId,
                createdAt: undefined as { lt: Date } | undefined,
            };

            // Apply cursor-based pagination
            if (cursor) {
                const cursorPost = await db.query.posts.findFirst({
                    where: { id: cursor },
                });
                if (cursorPost) {
                    whereCondition = { ...repliesFilter, userId, createdAt: { lt: cursorPost.createdAt } };
                }
            }

            feedPosts = await db.query.posts.findMany({
                where: whereCondition,
                with: feedPostRelations,
                orderBy: (posts, { desc }) => [desc(posts.createdAt)],
                limit,
            });
        } else if (type === 'curated') {
            // Curated feed - swarm posts only
            const session = await getSession().catch(() => null);
            const viewer = session?.user ?? null;
            const includeNsfw = shouldIncludeNsfwFeed({
                viewer,
                localNodeIsNsfw: await isLocalNodeNsfw(),
            });

            // Fetch swarm posts with user's NSFW preference
            const { fetchSwarmTimeline } = await import('@/lib/swarm/timeline');
            const cursorDate = await getMixedFeedCursorDate(cursor);
            const swarmResult = await fetchSwarmTimeline(10, 30, {
                includeNsfw,
                cursor: cursorDate?.toISOString(),
            });

            console.log('[Curated Feed] Swarm result:', {
                postsCount: swarmResult.posts.length,
                sources: swarmResult.sources,
                includeNsfw,
            });

            const swarmPosts = swarmResult.posts.map(mapSwarmPostToPost);

            let mutedIds = new Set<string>();
            let blockedIds = new Set<string>();

            if (viewer) {
                const muteRows = await db.select({ mutedUserId: mutes.mutedUserId })
                    .from(mutes)
                    .where(eq(mutes.userId, viewer.id));
                mutedIds = new Set(muteRows.map(row => row.mutedUserId));

                const blockRows = await db.select({ blockedUserId: blocks.blockedUserId })
                    .from(blocks)
                    .where(eq(blocks.userId, viewer.id));
                blockedIds = new Set(blockRows.map(row => row.blockedUserId));
            }

            const eligiblePosts = swarmPosts
                .filter((post) => !mutedIds.has(post.author.id) && !blockedIds.has(post.author.id));
            const rankedPosts = rankCuratedFeed(eligiblePosts, { limit });

            console.log('[Curated Feed] After ranking:', {
                swarmPostsCount: swarmPosts.length,
                afterMuteFilter: eligiblePosts.length,
                rankedPostsCount: rankedPosts.length,
                limit,
            });

            feedPosts = rankedPosts;
        } else {
            // Home timeline - need auth
            try {
                const user = await requireAuth();

                // Get IDs of users the current user follows
                const followRows = await db.select({ followingId: follows.followingId })
                    .from(follows)
                    .where(eq(follows.followerId, user.id));
                const followingIds = followRows.map(row => row.followingId);

                // Include own posts + posts from followed users
                const allowedUserIds = [user.id, ...followingIds];

                // Build where condition with cursor support
                let whereCondition = {
                    ...baseFilter,
                    userId: { in: allowedUserIds },
                    createdAt: undefined as { lt: Date } | undefined,
                };
                const cursorDate = await getMixedFeedCursorDate(cursor);

                if (cursorDate) {
                    whereCondition = { ...baseFilter, userId: { in: allowedUserIds }, createdAt: { lt: cursorDate } };
                }

                // Get local posts from people the user follows + their own posts
                const localPosts = await db.query.posts.findMany({
                    where: whereCondition,
                    with: feedPostRelations,
                    orderBy: (posts, { desc }) => [desc(posts.createdAt)],
                    limit: cursor ? limit : limit * 2, // Get more on first load to account for mixing with remote
                });

                const swarmRepostWhere = {
                    userId: { in: allowedUserIds },
                    ...(cursorDate ? { repostedAt: { lt: cursorDate } } : {}),
                };
                const swarmRepostRows = await db.query.userSwarmReposts.findMany({
                    where: swarmRepostWhere,
                    orderBy: (userSwarmReposts, { desc }) => [desc(userSwarmReposts.repostedAt)],
                    limit: cursor ? limit : limit * 2,
                });

                const swarmRepostAuthors = swarmRepostRows.length > 0
                    ? await db.query.users.findMany({
                        where: { id: { in: Array.from(new Set(swarmRepostRows.map((row) => row.userId))) } },
                    })
                    : [];
                const swarmRepostAuthorMap = new Map(swarmRepostAuthors.map((author) => [author.id, author]));
                const localRepostEvents = swarmRepostRows
                    .map((row) => {
                        const author = swarmRepostAuthorMap.get(row.userId);
                        if (!author) {
                            return null;
                        }
                        return mapUserSwarmRepostToFeedPost(row, author);
                    })
                    .filter(Boolean);

                // Get handles of remote users we follow
                const followedRemoteUsers = await db.query.remoteFollows.findMany({
                    where: { followerId: user.id },
                });

                // Fetch posts LIVE from followed remote users (in parallel, with timeout)
                let liveRemotePosts: any[] = [];
                if (followedRemoteUsers.length > 0) {
                    const { fetchSwarmUserProfile, isSwarmNode } = await import('@/lib/swarm/interactions');
                    const { mapRemoteProfilePost } = await import('@/lib/swarm/remote-profile-posts');

                    // Wrap each fetch with a timeout to prevent slow nodes from blocking
                    const withTimeout = <T>(promise: Promise<T>, ms: number): Promise<T | null> => {
                        return Promise.race([
                            promise,
                            new Promise<null>((resolve) => setTimeout(() => resolve(null), ms))
                        ]);
                    };

                    const fetchPromises = followedRemoteUsers.map(async (follow) => {
                        try {
                            const atIndex = follow.targetHandle.lastIndexOf('@');
                            if (atIndex === -1) return [];

                            const handle = follow.targetHandle.slice(0, atIndex);
                            const domain = follow.targetHandle.slice(atIndex + 1);

                            // Only fetch from swarm nodes
                            const isSwarm = await isSwarmNode(domain);
                            if (!isSwarm) return [];

                            const profileData = await withTimeout(
                                fetchSwarmUserProfile(handle, domain, limit, cursorDate?.toISOString()),
                                5000 // 5s timeout per node
                            );
                            if (!profileData?.posts) return [];

                            return profileData.posts
                                .filter((post: any) => !post.replyToId && !post.swarmReplyToId && !post.isReply)
                                .filter((post: any) => !cursorDate || new Date(post.createdAt) < cursorDate)
                                .map((post: any) => mapRemoteProfilePost({
                                    ...post,
                                    author: post.author || {
                                        handle,
                                        displayName: follow.displayName || profileData.profile?.displayName || handle,
                                        avatarUrl: follow.avatarUrl || profileData.profile?.avatarUrl,
                                        isBot: profileData.profile?.isBot,
                                    },
                                }, domain));
                        } catch (error) {
                            console.error(`[Home] Error fetching posts from ${follow.targetHandle}:`, error);
                            return [];
                        }
                    });

                    const results = await Promise.all(fetchPromises);
                    liveRemotePosts = results.flat();
                }

                // Merge and sort by date
                const allPosts = [...localPosts, ...localRepostEvents, ...liveRemotePosts]
                    .sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime())
                    .slice(0, limit);

                feedPosts = allPosts as any;
            } catch {
                // Not authenticated, return public timeline
                feedPosts = await db.query.posts.findMany({
                    where: baseFilter,
                    with: feedPostRelations,
                    orderBy: (posts, { desc }) => [desc(posts.createdAt)],
                    limit,
                });
            }
        }

        // Populate isLiked and isReposted for authenticated users
        try {
            const { getSession } = await import('@/lib/auth');
            const session = await getSession();

            if (session?.user && feedPosts && feedPosts.length > 0) {
                const viewer = session.user;
                const nodeDomain = process.env.NEXT_PUBLIC_NODE_DOMAIN || 'localhost:43821';
                const allFeedPosts = collectNestedPosts(feedPosts as FeedPostWithChildren[]);

                // Separate local and swarm posts
                const localPostIds: string[] = [];
                const swarmPosts: Array<{ id: string; domain: string; originalId: string }> = [];

                for (const p of allFeedPosts) {
                    if (p.id.startsWith('swarm:')) {
                        const parts = p.id.split(':');
                        if (parts.length >= 3) {
                            swarmPosts.push({
                                id: p.id,
                                domain: parts[1],
                                originalId: parts[2],
                            });
                        }
                    } else {
                        localPostIds.push(p.id);
                    }
                }

                // Check local likes
                const likedPostIds = new Set<string>();
                const repostedPostIds = new Set<string>();

                if (localPostIds.length > 0) {
                    const viewerLikes = await db.query.likes.findMany({
                        where: { AND: [{ userId: viewer.id }, { postId: { in: localPostIds } }] },
                    });
                    viewerLikes.forEach(l => likedPostIds.add(l.postId));

                    const viewerReposts = await db.query.posts.findMany({
                        where: { AND: [{ userId: viewer.id }, { repostOfId: { in: localPostIds } }, { isRemoved: false }] },
                    });
                    viewerReposts.forEach(r => { if (r.repostOfId) repostedPostIds.add(r.repostOfId); });
                }

                // Check swarm likes in real-time (query origin nodes)
                if (swarmPosts.length > 0) {
                    const { getViewerSwarmRepostedPostIds } = await import('@/lib/swarm/reposts');

                    const checkPromises = swarmPosts.map(async (sp) => {
                        try {
                            const protocol = sp.domain.includes('localhost') ? 'http' : 'https';
                            const url = `${protocol}://${sp.domain}/api/swarm/posts/${sp.originalId}/likes?checkHandle=${viewer.handle}&checkDomain=${nodeDomain}`;

                            const res = await fetch(url, {
                                headers: { 'Accept': 'application/json' },
                                signal: AbortSignal.timeout(3000),
                            });

                            if (res.ok) {
                                const data = await res.json();
                                if (data.isLiked) {
                                    likedPostIds.add(sp.id);
                                }
                            }
                        } catch (err) {
                            // Timeout or error - just skip
                        }
                    });

                    await Promise.all(checkPromises);

                    const swarmRepostedIds = await getViewerSwarmRepostedPostIds(
                        swarmPosts.map((sp) => ({
                            id: sp.id,
                            nodeDomain: sp.domain,
                            originalPostId: sp.originalId,
                        })),
                        viewer.id
                    );
                    swarmRepostedIds.forEach((id) => repostedPostIds.add(id));
                }

                feedPosts = applyInteractionFlags(
                    feedPosts as FeedPostWithChildren[],
                    likedPostIds,
                    repostedPostIds
                ) as any;
            }
        } catch (error) {
            console.error('Error populating interaction flags:', error);
        }

        return NextResponse.json({
            posts: feedPosts || [],
            meta: type === 'curated' ? {
                algorithm: 'curated-v2-diversity',
                windowHours: CURATED_FEED_WINDOW_HOURS,
                seedLimit: Math.min(limit * CURATION_SEED_MULTIPLIER, CURATION_SEED_CAP),
                weights: {
                    ...CURATED_FEED_WEIGHTS,
                },
            } : undefined,
            nextCursor: (feedPosts?.length === limit)
                ? (type === 'home' || type === 'curated'
                    ? encodeFeedCursor(feedPosts[feedPosts.length - 1]?.createdAt)
                    : feedPosts[feedPosts.length - 1]?.id)
                : null,
        });
    } catch (error) {
        console.error('Get feed error details:', error);
        return NextResponse.json(
            { error: 'Failed to get feed' },
            { status: 500 }
        );
    }
}
