import { NextRequest, NextResponse } from "next/server";
import {
  db,
  recipes,
  recipeIngredients,
  users,
  eq,
  and,
  or,
  ilike,
  sql,
  desc,
} from "@epicure/db";
import { getAvgRatingsByRecipeId } from "@/lib/recipe-ratings";
import { attachCardExtras } from "@/lib/recipe-card-extras";
import { getOptionalSession } from "@/lib/api-auth";

const VALID_DIETARY = ["vegan", "vegetarian", "glutenFree", "dairyFree"] as const;
type DietaryTag = (typeof VALID_DIETARY)[number];

export async function GET(req: NextRequest) {
  const { searchParams } = req.nextUrl;

  // --- Parse & validate required param ---
  const q = (searchParams.get("q") ?? "").trim().slice(0, 200);
  if (!q) {
    return NextResponse.json(
      { error: "Query parameter 'q' is required and must not be empty." },
      { status: 400 }
    );
  }

  // --- Optional params ---
  const difficultyParam = searchParams.get("difficulty");
  const difficulty =
    difficultyParam === "easy" ||
    difficultyParam === "medium" ||
    difficultyParam === "hard"
      ? (difficultyParam as "easy" | "medium" | "hard")
      : undefined;

  const maxMinsRaw = searchParams.get("maxMins");
  const maxMins =
    maxMinsRaw !== null && !Number.isNaN(Number(maxMinsRaw))
      ? Number(maxMinsRaw)
      : undefined;

  const limitRaw = searchParams.get("limit");
  const limit = Math.min(
    limitRaw !== null && !Number.isNaN(Number(limitRaw))
      ? Math.max(1, Number(limitRaw))
      : 20,
    50
  );

  const offsetRaw = searchParams.get("offset");
  const offset =
    offsetRaw !== null && !Number.isNaN(Number(offsetRaw))
      ? Math.max(0, Number(offsetRaw))
      : 0;

  const dietaryRaw = searchParams.get("dietary");
  const dietaryTags: DietaryTag[] = dietaryRaw
    ? (dietaryRaw
        .split(",")
        .map((s) => s.trim())
        .filter((s): s is DietaryTag =>
          (VALID_DIETARY as readonly string[]).includes(s)
        ))
    : [];

  // --- Build WHERE conditions ---
  // Escape ilike wildcard chars (% and _) so user input like "100%" is matched literally.
  const escapedQ = q.replace(/[\\%_]/g, (c) => `\\${c}`);

  // A query also matches if it's a substring of an ingredient's raw name or
  // one of the recipe's free-form tags — not just title/description. Neither
  // recipeIngredients.rawName nor recipes.tags has a supporting index today,
  // so this is a sequential scan; fine at this scale, worth a trigram/GIN
  // index if search ever gets slow.
  const conditions = [
    eq(recipes.visibility, "public"),
    eq(users.isPrivate, false),
    or(
      ilike(recipes.title, `%${escapedQ}%`),
      ilike(recipes.description, `%${escapedQ}%`),
      sql`exists (select 1 from ${recipeIngredients} where ${recipeIngredients.recipeId} = ${recipes.id} and ${recipeIngredients.rawName} ilike ${`%${escapedQ}%`})`,
      sql`exists (select 1 from unnest(${recipes.tags}) as tag where tag ilike ${`%${escapedQ}%`})`
    )!,
  ];

  if (difficulty) {
    conditions.push(eq(recipes.difficulty, difficulty));
  }

  const recipeTypeParam = searchParams.get("recipeType");
  const recipeType = recipeTypeParam === "dish" || recipeTypeParam === "drink" ? recipeTypeParam : undefined;
  if (recipeType) {
    conditions.push(eq(recipes.recipeType, recipeType));
  }

  if (maxMins !== undefined) {
    conditions.push(
      sql`(${recipes.prepMins} + ${recipes.cookMins}) <= ${maxMins}`
    );
  }

  for (const tag of dietaryTags) {
    // Containment (@>) instead of ->> text extraction so the GIN index on dietaryTags is actually used.
    conditions.push(sql`${recipes.dietaryTags} @> ${JSON.stringify({ [tag]: true })}::jsonb`);
  }

  // Exact-tag refinement (filter chips) — distinct from the free-text match
  // above, which only checks whether the query substring appears in a tag.
  const tagsRaw = searchParams.get("tags");
  const tagFilters = tagsRaw
    ? tagsRaw.split(",").map((s) => s.trim()).filter(Boolean).slice(0, 5)
    : [];
  for (const tag of tagFilters) {
    conditions.push(sql`${recipes.tags} @> ARRAY[${tag}]::text[]`);
  }

  const where = and(...conditions);

  // --- Main data query ---
  const rows = await db
    .select({
      id: recipes.id,
      title: recipes.title,
      description: recipes.description,
      difficulty: recipes.difficulty,
      baseServings: recipes.baseServings,
      prepMins: recipes.prepMins,
      cookMins: recipes.cookMins,
      visibility: recipes.visibility,
      tags: recipes.tags,
      isBatchCook: recipes.isBatchCook,
      sourceUrl: recipes.sourceUrl,
      recipeType: recipes.recipeType,
      coverIcon: recipes.coverIcon,
      coverColor: recipes.coverColor,
      authorId: recipes.authorId,
      authorName: users.name,
      createdAt: recipes.createdAt,
    })
    .from(recipes)
    .innerJoin(users, eq(recipes.authorId, users.id))
    .where(where)
    .orderBy(desc(recipes.createdAt))
    .limit(limit)
    .offset(offset);

  // --- Count query ---
  const countResult = await db
    .select({ total: sql<number>`count(*)::int` })
    .from(recipes)
    .innerJoin(users, eq(recipes.authorId, users.id))
    .where(where);

  const total = countResult[0]?.total ?? 0;

  const session = await getOptionalSession();
  const [ratingByRecipe, rowsWithExtras] = await Promise.all([
    getAvgRatingsByRecipeId(rows.map((r) => r.id)),
    attachCardExtras(rows, session?.user.id),
  ]);
  const data = rowsWithExtras.map((r) => ({
    ...r,
    avgRating: ratingByRecipe.get(r.id)?.avgRating ?? null,
    ratingCount: ratingByRecipe.get(r.id)?.ratingCount ?? 0,
  }));

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