import { NextRequest, NextResponse } from "next/server";
import { z } from "zod";
import { db, recipes, comments, users, userBlocks, eq, and, inArray, isNull, sql } from "@epicure/db";
import { requireSessionOrApiKey } from "@/lib/api-auth";
import { applyRateLimit } from "@/lib/rate-limit";
import { dispatchWebhook } from "@/lib/webhooks";
import { createNotification } from "@/lib/notifications";
import { isBlockedEitherWay } from "@/lib/blocks";
import { extractMentionedUsernames } from "@/lib/mentions";

const Schema = z.object({
  content: z.string().min(1).max(5000),
  parentId: z.string().optional(),
});

type Params = { params: Promise<{ id: string }> };

const COMMENT_COLUMNS = {
  id: comments.id,
  content: comments.content,
  parentId: comments.parentId,
  createdAt: comments.createdAt,
  updatedAt: comments.updatedAt,
  userId: comments.userId,
  userName: users.name,
  userUsername: users.username,
  userAvatarUrl: users.avatarUrl,
} as const;

export async function GET(req: NextRequest, { params }: Params) {
  const { id } = await params;
  const recipe = await db.query.recipes.findFirst({ where: eq(recipes.id, id) });
  if (!recipe || recipe.visibility === "private") {
    return NextResponse.json({ error: "Not found" }, { status: 404 });
  }

  const { session } = await requireSessionOrApiKey(req);

  const { searchParams } = req.nextUrl;
  const limitRaw = parseInt(searchParams.get("limit") ?? "20");
  const limit = Math.min(Number.isNaN(limitRaw) ? 20 : Math.max(1, limitRaw), 50);
  const offsetRaw = parseInt(searchParams.get("offset") ?? "0");
  const offset = Number.isNaN(offsetRaw) ? 0 : Math.max(0, offsetRaw);

  // Paginate top-level comments; replies to a loaded top-level comment are always
  // fetched alongside it so threads render complete (threads are typically small).
  const [topLevel, totalRow] = await Promise.all([
    db
      .select(COMMENT_COLUMNS)
      .from(comments)
      .innerJoin(users, eq(comments.userId, users.id))
      .where(and(eq(comments.recipeId, id), isNull(comments.parentId)))
      .orderBy(comments.createdAt)
      .limit(limit)
      .offset(offset),
    db
      .select({ total: sql<number>`count(*)::int` })
      .from(comments)
      .where(and(eq(comments.recipeId, id), isNull(comments.parentId))),
  ]);

  const total = totalRow[0]?.total ?? 0;
  const topLevelIds = topLevel.map((c) => c.id);

  const replies = topLevelIds.length > 0
    ? await db
        .select(COMMENT_COLUMNS)
        .from(comments)
        .innerJoin(users, eq(comments.userId, users.id))
        .where(inArray(comments.parentId, topLevelIds))
        .orderBy(comments.createdAt)
    : [];

  const rows = [...topLevel, ...replies];

  const data = session
    ? await (async () => {
        const blocked = await db
          .select({ blockedId: userBlocks.blockedId })
          .from(userBlocks)
          .where(eq(userBlocks.blockerId, session.user.id));
        const blockedIds = new Set(blocked.map((b) => b.blockedId));
        return rows.filter((r) => !blockedIds.has(r.userId));
      })()
    : rows;

  return NextResponse.json({ data, total, limit, offset });
}

export async function POST(req: NextRequest, { params }: Params) {
  const { session, response } = await requireSessionOrApiKey(req);
  if (response) return response;
  const { id } = await params;

  const limited = await applyRateLimit(`rl:comment:${session!.user.id}`, 20, 60);
  if (limited) return limited;

  const recipe = await db.query.recipes.findFirst({ where: eq(recipes.id, id) });
  if (!recipe || (recipe.visibility === "private" && recipe.authorId !== session!.user.id)) {
    return NextResponse.json({ error: "Not found" }, { status: 404 });
  }

  if (recipe.authorId !== session!.user.id && await isBlockedEitherWay(session!.user.id, recipe.authorId)) {
    return NextResponse.json({ error: "Not found" }, { status: 404 });
  }

  const body = await req.json() as unknown;
  const parsed = Schema.safeParse(body);
  if (!parsed.success) return NextResponse.json({ error: "Validation error" }, { status: 400 });

  let parent: { id: string; userId: string } | undefined;
  if (parsed.data.parentId) {
    parent = await db.query.comments.findFirst({
      where: and(eq(comments.id, parsed.data.parentId), eq(comments.recipeId, id)),
    });
    if (!parent) return NextResponse.json({ error: "Parent comment not found" }, { status: 404 });
  }

  const commentId = crypto.randomUUID();
  await db.insert(comments).values({
    id: commentId,
    recipeId: id,
    userId: session!.user.id,
    parentId: parsed.data.parentId,
    content: parsed.data.content,
  });

  // Dispatch to recipe author's webhooks (not to the commenter themselves).
  // Push/email to the author are handled by createNotification's "comment" call below.
  if (recipe.authorId !== session!.user.id) {
    void dispatchWebhook(recipe.authorId, "comment.added", { commentId, recipeId: id, recipeTitle: recipe.title });
  }

  if (parent && parent.userId !== session!.user.id) {
    void createNotification({ userId: parent.userId, type: "reply", actorId: session!.user.id, recipeId: id, commentId });
  }
  if (recipe.authorId !== parent?.userId) {
    void createNotification({ userId: recipe.authorId, type: "comment", actorId: session!.user.id, recipeId: id, commentId });
  }

  const mentionedUsernames = extractMentionedUsernames(parsed.data.content);
  if (mentionedUsernames.length > 0) {
    const alreadyNotified = new Set([recipe.authorId, parent?.userId].filter(Boolean));
    const mentionedUsers = await db
      .select({ id: users.id, username: users.username })
      .from(users)
      .where(inArray(users.username, mentionedUsernames));
    for (const mentioned of mentionedUsers) {
      if (alreadyNotified.has(mentioned.id)) continue;
      void createNotification({ userId: mentioned.id, type: "mention", actorId: session!.user.id, recipeId: id, commentId });
    }
  }

  return NextResponse.json({ id: commentId }, { status: 201 });
}
