import type { FastifyInstance } from "fastify";
import { and, desc, eq, gt, inArray, isNull, sql } from "drizzle-orm";
import { schema } from "@app/database";
import { whatToEatQuerySchema } from "@app/validation";
import {
  computeCoverage,
  checkRecipeSafety,
  isRecipeSafe,
  type IngredientSafetyInfo,
  type PantryItem,
} from "@app/recipe-engine";
import {
  isEventActive,
  parseCraving,
  rankAll,
  seasonForDate,
  summarizeContext,
  type RecommendationCandidate,
  type RecommendationContext,
} from "@app/recommendation-engine";
import { DEFAULT_TARGETS, computeDailyTargets, summarizeDay } from "@app/nutrition-engine";
import { parse } from "../lib/errors.js";
import { requireActiveHousehold, todayIso } from "../lib/helpers.js";

/**
 * "Vad ska vi äta?" (spec §18) – appens viktigaste endpoint.
 *
 * Pipeline:
 *  1. Hämta hushållets lager, medlemmarnas SAMLADE kostbegränsningar och kontext.
 *  2. Deterministisk säkerhetsfiltrering (spec §61.2) – blockers försvinner.
 *  3. Täckningsberäkning mot lagret + poängsättning med förklaringar.
 *  4. Matlådor rekommenderas före ny matlagning när rimligt (spec §24).
 *  5. (Bakom flagga) AAMOS får omranka topplistan – aldrig lägga till recept.
 */
export async function recommendationRoutes(app: FastifyInstance) {
  const auth = { preHandler: [app.authenticate] };

  app.get("/v1/recommendations/what-to-eat", auth, async (req) => {
    const q = parse(whatToEatQuerySchema, req.query);
    const householdId = await requireActiveHousehold(app.db, req.userId);
    const today = new Date();

    // --- 1. Kontext: lager ---
    const stockRows = await app.db
      .select({
        item: schema.inventoryItems,
        locationType: schema.storageLocations.type,
        shelfLife: schema.canonicalIngredients.shelfLifeGuidance,
        density: schema.canonicalIngredients.densityGPerMl,
        gramsPerPiece: schema.canonicalIngredients.gramsPerPiece,
      })
      .from(schema.inventoryItems)
      .innerJoin(
        schema.storageLocations,
        eq(schema.inventoryItems.storageLocationId, schema.storageLocations.id),
      )
      .leftJoin(
        schema.canonicalIngredients,
        eq(schema.inventoryItems.canonicalIngredientId, schema.canonicalIngredients.id),
      )
      .where(
        and(
          eq(schema.inventoryItems.householdId, householdId),
          isNull(schema.inventoryItems.depletedAt),
          gt(schema.inventoryItems.quantity, 0),
        ),
      );

    const pantry: PantryItem[] = stockRows.map((r) => ({
      id: r.item.id,
      canonicalIngredientId: r.item.canonicalIngredientId,
      quantity: r.item.quantity,
      unit: r.item.unit,
      bestBeforeDate: r.item.bestBeforeDate,
      useByDate: r.item.useByDate,
      openedAt: r.item.openedAt,
      frozenAt: r.item.frozenAt,
      thawedAt: r.item.thawedAt,
      purchasedAt: r.item.purchasedAt,
      storageLocationType: r.locationType,
      shelfLifeGuidance: r.shelfLife,
    }));
    const unitInfo = new Map(
      stockRows
        .filter((r) => r.item.canonicalIngredientId)
        .map((r) => [
          r.item.canonicalIngredientId!,
          { densityGPerMl: r.density, gramsPerPiece: r.gramsPerPiece },
        ]),
    );

    // --- 2. Hushållets samlade begränsningar (spec §7: strängaste gäller) ---
    const members = await app.db
      .select({ userId: schema.householdMembers.userId })
      .from(schema.householdMembers)
      .where(eq(schema.householdMembers.householdId, householdId));
    const memberIds = members.map((m) => m.userId);
    const allPrefs = await app.db
      .select()
      .from(schema.userPreferences)
      .where(inArray(schema.userPreferences.userId, memberIds));

    const combinedAllergens = [...new Set(allPrefs.flatMap((p) => p.allergens))];
    const combinedAvoid = [...new Set(allPrefs.flatMap((p) => p.avoidIngredientIds))];
    const strictestSpice = Math.min(...allPrefs.map((p) => p.spiceLevelMax), 5);
    const myPrefs = allPrefs.find((p) => p.userId === req.userId);

    // --- 3. Kandidater: publicerade recept för måltidstypen ---
    const candidates = await app.db
      .select()
      .from(schema.recipes)
      .where(
        and(
          eq(schema.recipes.status, "published"),
          sql`${q.mealType} = ANY(${schema.recipes.mealTypes})`,
        ),
      )
      .limit(200);

    const allIngredients = await app.db
      .select()
      .from(schema.recipeIngredients)
      .where(
        inArray(
          schema.recipeIngredients.recipeId,
          candidates.map((c) => c.id),
        ),
      );
    const ingredientIds = [...new Set(allIngredients.map((i) => i.canonicalIngredientId))];
    const safetyRows = await app.db
      .select()
      .from(schema.canonicalIngredients)
      .where(inArray(schema.canonicalIngredients.id, ingredientIds));
    const safetyMap = new Map<string, IngredientSafetyInfo>(
      safetyRows.map((r) => [
        r.id,
        {
          id: r.id,
          allergens: r.allergens,
          isVegan: r.isVegan,
          isVegetarian: r.isVegetarian,
          containsGluten: r.containsGluten,
          containsLactose: r.containsLactose,
          isPork: r.isPork,
          isBeef: r.isBeef,
          isAlcohol: r.isAlcohol,
        },
      ]),
    );
    for (const r of safetyRows) {
      if (!unitInfo.has(r.id)) {
        unitInfo.set(r.id, { densityGPerMl: r.densityGPerMl, gramsPerPiece: r.gramsPerPiece });
      }
    }

    // --- 4. Näringskontext: vad återstår av dagen? ---
    const [profile] = await app.db
      .select()
      .from(schema.userHealthProfiles)
      .where(eq(schema.userHealthProfiles.userId, req.userId))
      .limit(1);
    const targets =
      profile?.weightKg && profile.heightCm && profile.birthYear
        ? computeDailyTargets({
            sex: profile.sex ?? "unspecified",
            age: today.getUTCFullYear() - profile.birthYear,
            heightCm: profile.heightCm,
            weightKg: profile.weightKg,
            activityLevel: profile.activityLevel,
            primaryGoal: myPrefs?.primaryGoal ?? undefined,
          }).targets
        : DEFAULT_TARGETS;
    const todaysMeals = await app.db
      .select({ nutrition: schema.meals.nutrition })
      .from(schema.meals)
      .where(and(eq(schema.meals.userId, req.userId), eq(schema.meals.date, todayIso())));
    const daySummary = summarizeDay(
      todaysMeals.map((m) => m.nutrition),
      targets,
    );

    // --- 5. Säsong & högtid (spec §28) ---
    const events = await app.db
      .select()
      .from(schema.seasonEvents)
      .where(and(eq(schema.seasonEvents.active, true), eq(schema.seasonEvents.market, "SE")));
    const activeHolidayTags = events
      .filter((e) => isEventActive({ dateRule: e.dateRule, leadDays: e.leadDays }, today))
      .map((e) => e.slug);

    // --- 6. Senast lagat (variation) + hushållsbetyg ---
    const cooks = await app.db
      .select({
        recipeId: schema.recipeCooks.recipeId,
        last: sql<string>`max(${schema.recipeCooks.cookedAt})`,
      })
      .from(schema.recipeCooks)
      .where(eq(schema.recipeCooks.householdId, householdId))
      .groupBy(schema.recipeCooks.recipeId);
    const lastCooked = new Map(cooks.map((c) => [c.recipeId, c.last]));
    const householdRatings = await app.db
      .select({
        recipeId: schema.recipeRatings.recipeId,
        avg: sql<number>`avg(${schema.recipeRatings.stars})`,
      })
      .from(schema.recipeRatings)
      .where(inArray(schema.recipeRatings.userId, memberIds))
      .groupBy(schema.recipeRatings.recipeId);
    const householdRatingMap = new Map(householdRatings.map((r) => [r.recipeId, Number(r.avg)]));

    // --- 7. Tolka "jag är sugen på" (spec §19) ---
    const craving = q.craving ? parseCraving(q.craving) : null;

    const ctx: RecommendationContext = {
      mealType: q.mealType,
      persons: q.persons ?? members.length,
      maxMinutes: q.maxMinutes ?? myPrefs?.maxCookingMinutesWeekday ?? undefined,
      maxCostMinorPerPortion: q.maxCostMinorPerPortion,
      remainingProteinG: Math.max(0, daySummary.remaining.proteinG),
      remainingKcal: Math.max(0, daySummary.remaining.kcal),
      currentSeason: seasonForDate(today),
      activeHolidayTags,
      isWeekday: today.getUTCDay() >= 1 && today.getUTCDay() <= 4,
      favoriteCuisines: myPrefs?.favoriteCuisines ?? [],
      cravingTags: craving?.tags,
      cravingCuisine: craving?.cuisine,
      cravingMaxKcal: craving?.maxKcal,
    };

    // --- 8. Filtrera säkert + beräkna täckning + poängsätt ---
    const scoredCandidates: RecommendationCandidate[] = [];
    for (const recipe of candidates) {
      const recipeIngredients = allIngredients
        .filter((i) => i.recipeId === recipe.id)
        .map((i) => ({
          canonicalIngredientId: i.canonicalIngredientId,
          displayNameSv: i.displayNameSv,
          quantity: i.quantity,
          unit: i.unit,
          optional: i.optional,
        }));

      const violations = checkRecipeSafety(
        {
          ingredients: recipeIngredients.map((i) => ({
            canonicalIngredientId: i.canonicalIngredientId,
            optional: i.optional,
          })),
          spiceLevel: recipe.spiceLevel,
        },
        {
          allergens: combinedAllergens,
          dietPattern: myPrefs?.dietPattern,
          religiousRule: myPrefs?.religiousRule,
          avoidIngredientIds: combinedAvoid,
          spiceLevelMax: strictestSpice,
        },
        safetyMap,
      );
      if (!isRecipeSafe(violations)) continue;

      const coverage = computeCoverage(recipeIngredients, pantry, unitInfo, today);
      const lastDate = lastCooked.get(recipe.id);
      scoredCandidates.push({
        recipeId: recipe.id,
        titleSv: recipe.titleSv,
        cuisine: recipe.cuisine,
        tags: recipe.tags as never,
        totalTimeMinutes: recipe.totalTimeMinutes,
        nutritionPerPortion: recipe.nutritionPerPortion,
        estimatedCostMinorPerPortion: recipe.estimatedCostMinorPerPortion,
        ratingAverage: recipe.ratingAverage,
        ratingCount: recipe.ratingCount,
        peakSeasons: recipe.peakSeasons,
        holidayTags: recipe.holidayTags,
        spiceLevel: recipe.spiceLevel,
        coverage,
        daysSinceLastCooked: lastDate
          ? Math.floor((today.getTime() - Date.parse(lastDate)) / 86_400_000)
          : null,
        householdRating: householdRatingMap.get(recipe.id) ?? null,
      });
    }

    let recommendations = rankAll(scoredCandidates, ctx, undefined, q.limit);

    // --- 9. AAMOS-omrankning bakom feature flag (aldrig obligatorisk) ---
    if (await app.flags.isEnabled("ai_rerank", req.userId)) {
      const result = await app.aamos.runTask(
        "RANK_RECIPES",
        {
          candidateIds: recommendations.map((r) => r.recipeId),
          deterministicScores: Object.fromEntries(
            recommendations.map((r) => [r.recipeId, r.score]),
          ),
          contextSummary: summarizeContext(ctx),
        },
        {
          correlationId: req.correlationId,
          subjectRef: null,
          localeContext: await (
            await import("../lib/localeContext.js")
          ).getLocaleContext(app.db, req.userId),
        },
      );
      if (result.status === "ok" && result.output) {
        const order = new Map(result.output.rankedIds.map((id, i) => [id, i]));
        recommendations = [...recommendations].sort(
          (a, b) => (order.get(a.recipeId) ?? 99) - (order.get(b.recipeId) ?? 99),
        );
      }
    }

    // --- 10. Matlådor först när rimligt (spec §24) ---
    const mealBoxes = q.includeLeftovers
      ? await app.db
          .select()
          .from(schema.mealBoxes)
          .where(
            and(
              eq(schema.mealBoxes.householdId, householdId),
              eq(schema.mealBoxes.status, "available"),
            ),
          )
          .orderBy(schema.mealBoxes.recommendedUseBy)
          .limit(5)
      : [];
    const mealBoxSuggestions = mealBoxes.map((box) => ({
      mealBoxId: box.id,
      titleSv: box.titleSv,
      portionsRemaining: box.portionsRemaining,
      recommendedUseBy: box.recommendedUseBy,
      whySv:
        Date.parse(box.recommendedUseBy) <= today.getTime() + 2 * 86_400_000
          ? `Matlådan bör ätas senast ${box.recommendedUseBy}. Noll matlagning, noll svinn.`
          : "Färdig mat som väntar – snabbaste middagen i huset.",
    }));

    return {
      mealType: q.mealType,
      context: {
        persons: ctx.persons,
        season: ctx.currentSeason,
        activeHolidays: activeHolidayTags,
        remainingKcal: ctx.remainingKcal,
        remainingProteinG: ctx.remainingProteinG,
        craving: craving ?? null,
      },
      mealBoxSuggestions,
      recommendations,
    };
  });
}
