import { and, eq, gt, inArray, isNull, sql } from "drizzle-orm";
import { schema } from "@app/database";
import {
  computeCoverage,
  checkRecipeSafety,
  isRecipeSafe,
  type IngredientSafetyInfo,
  type PantryItem,
} from "@app/recipe-engine";
import type { MealType } from "@app/shared-types";
import type { WorkerContext } from "../context.js";

interface WeekPlanJobInput {
  weekPlanId: string;
  householdId: string;
  userId: string;
  input: {
    weekStartDate: string;
    daysToPlann?: number;
    mealTypes: MealType[];
    mealSlots?: MealType[][];
    portionsPerMeal?: number;
    budgetMinorTotal?: number;
    preferLeftoversFirst: boolean;
    varietyLevel: "low" | "medium" | "high";
  };
}

/**
 * Veckoplansgenerering (spec §25). Deterministisk kärna:
 * 1. Matlådor planeras först (spec §24) när preferLeftoversFirst.
 * 2. Recept väljs på täckning + utgångsdatum + variation + budget.
 * 3. Samma recept upprepas inte inom planen (styrs av varietyLevel).
 * AAMOS (GENERATE_WEEK_PLAN) kan förfina ordningen men aldrig bryta reglerna.
 */
export async function processGenerateWeekPlan(
  ctx: WorkerContext,
  data: WeekPlanJobInput,
): Promise<void> {
  const { weekPlanId, householdId } = data;
  const [plan] = await ctx.db
    .select()
    .from(schema.weekPlans)
    .where(eq(schema.weekPlans.id, weekPlanId))
    .limit(1);
  if (!plan) return;

  const days = data.input.daysToPlann ?? 7;
  const mealTypes = data.input.mealTypes;
  const portions = data.input.portionsPerMeal ?? (await defaultPortions(ctx, householdId));

  // --- Lager & säkerhet ---
  const stockRows = await ctx.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 },
      ]),
  );

  const members = await ctx.db
    .select({ userId: schema.householdMembers.userId })
    .from(schema.householdMembers)
    .where(eq(schema.householdMembers.householdId, householdId));
  const prefs = await ctx.db
    .select()
    .from(schema.userPreferences)
    .where(
      inArray(
        schema.userPreferences.userId,
        members.map((m) => m.userId),
      ),
    );
  const combinedAllergens = [...new Set(prefs.flatMap((p) => p.allergens))];
  const combinedAvoid = [...new Set(prefs.flatMap((p) => p.avoidIngredientIds))];
  const strictestSpice = prefs.length > 0 ? Math.min(...prefs.map((p) => p.spiceLevelMax)) : 5;

  // --- Kandidater ---
  const candidates = await ctx.db
    .select()
    .from(schema.recipes)
    .where(and(eq(schema.recipes.status, "published")))
    .limit(300);
  const allIngredients = await ctx.db
    .select()
    .from(schema.recipeIngredients)
    .where(
      inArray(
        schema.recipeIngredients.recipeId,
        candidates.map((c) => c.id),
      ),
    );
  const safetyRows = await ctx.db
    .select()
    .from(schema.canonicalIngredients)
    .where(
      inArray(schema.canonicalIngredients.id, [
        ...new Set(allIngredients.map((i) => i.canonicalIngredientId)),
      ]),
    );
  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 });
  }

  // Variation mellan veckor OCH mellan omgenereringar: en liten deterministisk
  // "jitter" per recept, seedad på planens id + veckostart. Täckning och
  // utgångsdatum dominerar fortfarande (jitter är liten), men bland likvärdiga
  // rätter växlar urvalet så två veckor – och en "planera om" – inte blir
  // identiska. Nivån styrs av varietyLevel (kriteriet användaren valt).
  const varietyAmp =
    data.input.varietyLevel === "high" ? 0.35 : data.input.varietyLevel === "low" ? 0.1 : 0.2;
  const varietySeed = `${weekPlanId}|${data.input.weekStartDate}`;

  const scored = candidates
    .filter((recipe) => {
      const ings = allIngredients.filter((i) => i.recipeId === recipe.id);
      const violations = checkRecipeSafety(
        {
          ingredients: ings.map((i) => ({
            canonicalIngredientId: i.canonicalIngredientId,
            optional: i.optional,
          })),
          spiceLevel: recipe.spiceLevel,
        },
        {
          allergens: combinedAllergens,
          avoidIngredientIds: combinedAvoid,
          spiceLevelMax: strictestSpice,
        },
        safetyMap,
      );
      return isRecipeSafe(violations);
    })
    .map((recipe) => {
      const ings = 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 coverage = computeCoverage(ings, pantry, unitInfo);
      const expiryBoost = coverage.expiringUsed.length > 0 ? 0.3 : 0;
      const budgetOk =
        data.input.budgetMinorTotal == null ||
        recipe.estimatedCostMinorPerPortion == null ||
        recipe.estimatedCostMinorPerPortion * portions <=
          (data.input.budgetMinorTotal / (days * mealTypes.length)) * 1.5;
      return {
        recipe,
        coverage,
        score:
          coverage.coverage +
          expiryBoost +
          (budgetOk ? 0 : -0.5) +
          varietyAmp * hashUnit(`${recipe.id}|${varietySeed}`),
      };
    })
    .sort((a, b) => b.score - a.score);

  // --- Matlådor först (spec §24) ---
  const mealBoxes = data.input.preferLeftoversFirst
    ? await ctx.db
        .select()
        .from(schema.mealBoxes)
        .where(
          and(
            eq(schema.mealBoxes.householdId, householdId),
            eq(schema.mealBoxes.status, "available"),
          ),
        )
        .orderBy(schema.mealBoxes.recommendedUseBy)
    : [];

  // --- Bygg planen ---
  const usedRecipeIds = new Set<string>();
  const repeatLimit = data.input.varietyLevel === "low" ? 2 : 1;
  const recipeUseCount = new Map<string, number>();
  let boxIndex = 0;
  let candidateIndex = 0;
  const entries: Array<typeof schema.weekPlanEntries.$inferInsert> = [];

  // Variationshjälpare: undvik två soppor / samma kök i rad, och styr soppor
  // mot lunch snarare än middag.
  const isSoup = (r: { titleSv: string }) => /sopp|soup/i.test(r.titleSv);
  const signatureOf = (r: { cuisine: string; titleSv: string }) =>
    `${r.cuisine}|${isSoup(r) ? "soup" : "dish"}`;
  let lastSignature: string | null = null;

  // Tillbehör/sides (t.ex. potatisgratäng, mos, pommes) är inte en hel middag
  // i sig. Om titeln redan innehåller ett protein (köttfärs-/korvgratäng) är den
  // komplett. Annars föreslår vi en proteinbit att servera till.
  const proteinWords =
    /kött|korv|kyckling|lax|fisk|fläsk|skinka|biff|färs|räk|tofu|halloumi|ägg|bön|lins|kikärt|falafel|quorn|chorizo|bacon|lamm|anka|kalkon|tonfisk|torsk|sill/i;
  const sideWords = /gratäng|potatismos|rotmos|pommes|klyftpotatis|potatisstomp|potatissallad/i;
  const isSideDish = (r: { titleSv: string }) =>
    sideWords.test(r.titleSv) && !proteinWords.test(r.titleSv);
  const sideSuggestion =
    "Tillbehör – servera med en proteinbit: t.ex. en stekt eller grillad ryggbiff eller entrecôte, kyckling, fisk, eller tofu/halloumi för en vegetarisk variant.";

  for (let day = 0; day < days; day++) {
    const date = new Date(Date.parse(data.input.weekStartDate) + day * 86_400_000)
      .toISOString()
      .slice(0, 10);
    const dayMeals = data.input.mealSlots ? (data.input.mealSlots[day] ?? []) : mealTypes;
    for (const mealType of dayMeals) {
      // Matlåda om det finns och portionerna räcker
      const box = mealBoxes[boxIndex];
      if (box && box.portionsRemaining >= Math.min(portions, 2)) {
        entries.push({
          weekPlanId,
          date,
          mealType,
          mealBoxId: box.id,
          titleSv: `${box.titleSv} (matlåda)`,
          portions: Math.min(box.portionsRemaining, portions),
          status: "planned",
          rescheduleReasonSv: `Matlådan bör användas senast ${box.recommendedUseBy}.`,
          sortOrder: entries.length,
        });
        boxIndex++;
        lastSignature = null;
        continue;
      }
      // Nästa bästa recept: passa måltidstyp, undvik upprepning i rad och
      // soppa på middag. Lätta reglerna stegvis så en slot ALLTID fylls om
      // det finns någon rätt av rätt måltidstyp (inga tomma dagar).
      const pick = (o: {
        avoidSig: string | null;
        noSoupAtDinner: boolean;
        respectRepeat: boolean;
      }): { c: (typeof scored)[number]; idx: number } | null => {
        for (let i = 0; i < scored.length; i++) {
          const idx = (candidateIndex + i) % scored.length;
          const c = scored[idx]!;
          if (!c.recipe.mealTypes.includes(mealType)) continue;
          if (o.respectRepeat && (recipeUseCount.get(c.recipe.id) ?? 0) >= repeatLimit) continue;
          if (o.avoidSig && signatureOf(c.recipe) === o.avoidSig) continue;
          if (o.noSoupAtDinner && mealType === "dinner" && isSoup(c.recipe)) continue;
          return { c, idx };
        }
        return null;
      };
      const hit =
        pick({ avoidSig: lastSignature, noSoupAtDinner: true, respectRepeat: true }) ??
        pick({ avoidSig: lastSignature, noSoupAtDinner: false, respectRepeat: true }) ??
        pick({ avoidSig: null, noSoupAtDinner: false, respectRepeat: true }) ??
        pick({ avoidSig: lastSignature, noSoupAtDinner: false, respectRepeat: false }) ??
        pick({ avoidSig: null, noSoupAtDinner: false, respectRepeat: false });
      if (!hit) continue;
      const chosen = hit.c;
      candidateIndex = hit.idx + 1;
      lastSignature = signatureOf(chosen.recipe);
      recipeUseCount.set(chosen.recipe.id, (recipeUseCount.get(chosen.recipe.id) ?? 0) + 1);
      usedRecipeIds.add(chosen.recipe.id);
      const expiring = chosen.coverage.expiringUsed[0];
      const notes: string[] = [];
      if (isSideDish(chosen.recipe) && (mealType === "lunch" || mealType === "dinner")) {
        notes.push(sideSuggestion);
      }
      if (expiring) {
        notes.push(`Använder ${expiring.displayNameSv.toLowerCase()} som bör ätas snart.`);
      }
      entries.push({
        weekPlanId,
        date,
        mealType,
        recipeId: chosen.recipe.id,
        titleSv: chosen.recipe.titleSv,
        portions,
        status: "planned",
        rescheduleReasonSv: notes.length > 0 ? notes.join(" ") : null,
        sortOrder: entries.length,
      });
    }
  }

  await ctx.db
    .delete(schema.weekPlanEntries)
    .where(eq(schema.weekPlanEntries.weekPlanId, weekPlanId));
  if (entries.length > 0) await ctx.db.insert(schema.weekPlanEntries).values(entries);
  await ctx.db
    .update(schema.weekPlans)
    .set({ status: "draft", updatedAt: new Date() })
    .where(eq(schema.weekPlans.id, weekPlanId));

  await ctx.db.insert(schema.domainEvents).values({
    type: "WEEK_PLAN_UPDATED",
    userId: data.userId,
    householdId,
    payload: { weekPlanId, reason: "generated" },
  });
}

/** Deterministisk sträng-hash → [0,1). FNV-1a. Ger reproducerbar variation. */
function hashUnit(s: string): number {
  let h = 2166136261 >>> 0;
  for (let i = 0; i < s.length; i++) {
    h ^= s.charCodeAt(i);
    h = Math.imul(h, 16777619) >>> 0;
  }
  return (h >>> 0) / 4294967296;
}

async function defaultPortions(ctx: WorkerContext, householdId: string): Promise<number> {
  const [row] = await ctx.db
    .select({ total: sql<number>`coalesce(sum(${schema.householdMembers.portionFactor}), 2)` })
    .from(schema.householdMembers)
    .where(eq(schema.householdMembers.householdId, householdId));
  return Math.max(1, Math.round(Number(row?.total ?? 2)));
}
