import type { FastifyInstance } from "fastify";
import { and, asc, eq, gt, isNull, sql } from "drizzle-orm";
import { schema, markMilestone } from "@app/database";
import {
  allocateFefo,
  computeBalance,
  updateCookingAssumptionProfile,
  rollbackCookingAssumptionProfile,
} from "@app/inventory-engine";
import { scaleNutrition } from "@app/nutrition-engine";
import type { Unit } from "@app/shared-types";
import { MEAL_TYPES } from "@app/shared-types";
import {
  cookingSessionStarted,
  cookingSessionCompleted,
  cookingSessionUndone,
  leftoversCreated,
} from "@app/analytics";
import { todayIso, emitEvent, trackProductAnalytics } from "./helpers.js";
import { errors } from "./errors.js";
import { loadFullRecipe } from "../routes/recipes.js";
import { userLanguageTag } from "./contentLanguage.js";
import { t } from "./i18n.js";

export interface CompleteCookingInput {
  portionsCooked?: number;
  mealBoxPortions?: number;
  mealBoxFrozen?: boolean;
  mealBoxStorageLocationId?: string;
  deductInventory?: boolean;
  eaters?: Array<{ userId: string; portionFraction: number }>;
  date?: string;
  mealType?: string;
  inventoryOverrides?: Array<{ canonicalIngredientId: string; quantityUsed: number; unit: string }>;
  actualPortionsEaten?: number;
  leftoverEstimatePortions?: number;
  leftoverNote?: string;
}

export interface CompleteCookingSessionOptions {
  /** Sant för legacy POST /v1/recipes/:id/cook där session skapas och startas i ett steg. */
  emitStartedEvent?: boolean;
}

export interface CompleteCookingResult {
  ok: boolean;
  mealIds: string[];
  mealBoxId: string | null;
  mealBoxMutations: Array<{ mealBoxId: string; deltaPortions: number; frozen: boolean }>;
  inventoryDeductions: Array<{ itemId: string; quantity: number; unit: string; name: string }>;
  recipeIngredients: Array<{
    canonicalIngredientId: string;
    displayName: string;
    quantity: number;
    unit: string;
    optional: boolean;
  }>;
}

/**
 * Delad kompletteringsväg för både POST /v1/recipes/:id/cook (legacy)
 * och POST /v1/cooking-sessions/:id/complete. Hanterar summavalidering,
 * persistens av svar, profiluppdateringar och analytics.
 */
export async function completeCookingSession(
  app: FastifyInstance,
  session: typeof schema.cookingSessions.$inferSelect,
  userId: string,
  input: CompleteCookingInput,
  correlationId: string,
  options: CompleteCookingSessionOptions = {},
): Promise<CompleteCookingResult & { session: typeof schema.cookingSessions.$inferSelect }> {
  const plannedPortions = input.portionsCooked ?? session.plannedPortions;
  const mealBoxPortions = input.mealBoxPortions ?? 0;
  const actualPortionsEaten =
    input.actualPortionsEaten ?? Math.max(0, plannedPortions - mealBoxPortions);
  const leftoverEstimatePortions = input.leftoverEstimatePortions ?? mealBoxPortions;

  if (actualPortionsEaten + leftoverEstimatePortions > plannedPortions) {
    const languageTag = await userLanguageTag(app.db, userId);
    throw errors.badRequest(t("cooked.portionsSumExceedsPlanned", languageTag));
  }

  if (leftoverEstimatePortions < mealBoxPortions) {
    const languageTag = await userLanguageTag(app.db, userId);
    throw errors.badRequest(t("cooked.leftoverLessThanBox", languageTag));
  }

  if (options.emitStartedEvent && session.status === "started") {
    await trackProductAnalytics(
      app.db,
      userId,
      cookingSessionStarted({
        householdId: session.householdId,
        properties: {
          cookingSessionId: session.id,
          recipeId: session.recipeId,
          status: session.status,
          plannedPortions,
        },
      }),
    );
  }

  const mealDate = input.date ?? todayIso();
  const result = await completeCookingSessionCore(
    app,
    session,
    userId,
    {
      ...input,
      portionsCooked: plannedPortions,
      mealBoxPortions,
      actualPortionsEaten,
      leftoverEstimatePortions,
      date: mealDate,
    },
    correlationId,
  );

  // Persistera användarens svar (ersätter kärnans default-värden).
  const [updated] = await app.db
    .update(schema.cookingSessions)
    .set({
      actualPortionsEaten,
      leftoverEstimatePortions,
      leftoverNote: input.leftoverNote ?? null,
      mealDate,
      mealBoxMutations: result.mealBoxMutations,
      updatedAt: new Date(),
    })
    .where(eq(schema.cookingSessions.id, session.id))
    .returning();
  if (!updated) throw errors.internal("Kunde inte uppdatera cooking session.");

  // Uppdatera antagandeprofiler per icke-valfri ingrediens (hushållsnivå).
  const date = new Date().toISOString().slice(0, 10);
  for (const ing of result.recipeIngredients) {
    if (ing.optional) continue;
    const [existing] = await app.db
      .select()
      .from(schema.cookingAssumptionProfiles)
      .where(
        and(
          eq(schema.cookingAssumptionProfiles.householdId, session.householdId),
          eq(schema.cookingAssumptionProfiles.canonicalIngredientId, ing.canonicalIngredientId),
        ),
      )
      .limit(1);

    const updatedProfile = updateCookingAssumptionProfile(
      {
        householdId: session.householdId,
        canonicalIngredientId: ing.canonicalIngredientId,
        plannedPortions,
        actualPortionsEaten,
        leftoverEstimatePortions,
        sessionId: session.id,
        date,
      },
      existing ?? {
        averageEatenPortions: null,
        averageLeftoverPortions: null,
        observationCount: 0,
      },
    );

    await app.db
      .insert(schema.cookingAssumptionProfiles)
      .values({
        householdId: session.householdId,
        canonicalIngredientId: ing.canonicalIngredientId,
        ...updatedProfile,
        updatedAt: new Date(),
      })
      .onConflictDoUpdate({
        target: [
          schema.cookingAssumptionProfiles.householdId,
          schema.cookingAssumptionProfiles.canonicalIngredientId,
        ],
        set: {
          averageEatenPortions: updatedProfile.averageEatenPortions,
          averageLeftoverPortions: updatedProfile.averageLeftoverPortions,
          observationCount: updatedProfile.observationCount,
          lastSessionAnswers: updatedProfile.lastSessionAnswers,
          updatedAt: new Date(),
        },
      });
  }

  await trackProductAnalytics(
    app.db,
    userId,
    cookingSessionCompleted({
      householdId: session.householdId,
      properties: {
        cookingSessionId: session.id,
        recipeId: session.recipeId,
        portionsCooked: plannedPortions,
        actualPortionsEaten,
        leftoverEstimatePortions,
        mealBoxPortions,
      },
    }),
  );

  return { ...result, session: updated };
}

/**
 * Lågnivå-kärna för att "jag har lagat". Används via completeCookingSession
 * av både POST /v1/recipes/:id/cook (legacy shortcut) och
 * POST /v1/cooking-sessions/:id/complete.
 */
export async function completeCookingSessionCore(
  app: FastifyInstance,
  session: typeof schema.cookingSessions.$inferSelect,
  userId: string,
  input: CompleteCookingInput,
  correlationId: string,
): Promise<CompleteCookingResult> {
  const recipe = await loadFullRecipe(app, session.recipeId);
  const householdId = session.householdId;
  const portionsCooked = input.portionsCooked ?? session.plannedPortions;
  const date = input.date ?? todayIso();
  const mealType = (input.mealType ?? session.plannedMealType) as (typeof MEAL_TYPES)[number];

  // 1. FEFO-avdrag
  const deductions: Array<{ itemId: string; quantity: number; unit: string; name: string }> = [];
  if (input.deductInventory !== false) {
    // Partiell förbrukning (Fas 3 §6.3): råvaror dras för de tillagade
    // portionerna = ätna + rester (inklusive matlådor). Endast portioner som
    // aldrig tillagats stannar kvar som råvara i lagret.
    const actualPortionsEaten = input.actualPortionsEaten ?? portionsCooked;
    const leftoverEstimatePortions = input.leftoverEstimatePortions ?? 0;
    const consumptionPortions = Math.min(
      portionsCooked,
      actualPortionsEaten + leftoverEstimatePortions,
    );
    const factor = consumptionPortions / recipe.portions;
    const overrides = new Map(
      input.inventoryOverrides?.map((o) => [o.canonicalIngredientId, o]) ?? [],
    );

    for (const ing of recipe.ingredients) {
      if (ing.optional) continue;
      const override = overrides.get(ing.canonicalIngredientId);
      const requiredQty = override ? override.quantityUsed : ing.quantity * factor;
      const requiredUnit = override ? override.unit : ing.unit;
      if (requiredQty <= 0) continue;

      const stock = await app.db
        .select()
        .from(schema.inventoryItems)
        .where(
          and(
            eq(schema.inventoryItems.householdId, householdId),
            eq(schema.inventoryItems.canonicalIngredientId, ing.canonicalIngredientId),
            isNull(schema.inventoryItems.depletedAt),
            gt(schema.inventoryItems.quantity, 0),
          ),
        );
      if (stock.length === 0) continue;

      const [info] = await app.db
        .select()
        .from(schema.canonicalIngredients)
        .where(eq(schema.canonicalIngredients.id, ing.canonicalIngredientId))
        .limit(1);

      const allocation = allocateFefo(
        requiredQty,
        requiredUnit as Unit,
        stock.map((s) => ({
          id: s.id,
          canonicalIngredientId: s.canonicalIngredientId,
          quantity: s.quantity,
          unit: s.unit,
          bestBeforeDate: s.bestBeforeDate,
          useByDate: s.useByDate,
          openedAt: s.openedAt,
          frozenAt: s.frozenAt,
          thawedAt: s.thawedAt,
          purchasedAt: s.purchasedAt,
        })),
        { densityGPerMl: info?.densityGPerMl, gramsPerPiece: info?.gramsPerPiece },
      );

      for (const alloc of allocation.allocations) {
        const item = stock.find((s) => s.id === alloc.itemId)!;
        const newQty = Math.max(0, Math.round((item.quantity - alloc.quantity) * 1000) / 1000);
        await app.db.insert(schema.inventoryTransactions).values({
          householdId,
          inventoryItemId: alloc.itemId,
          cookingSessionId: session.id,
          type: "cook_use",
          quantityDelta: -(item.quantity - newQty),
          unit: item.unit,
          refType: "recipe_cook",
          refId: session.recipeId,
          actorUserId: userId,
        });
        await app.db
          .update(schema.inventoryItems)
          .set({
            quantity: newQty,
            depletedAt: newQty <= 0 ? new Date() : null,
            updatedAt: new Date(),
          })
          .where(eq(schema.inventoryItems.id, alloc.itemId));
        deductions.push({
          itemId: alloc.itemId,
          quantity: alloc.quantity,
          unit: alloc.unit,
          name: item.displayName,
        });
      }
    }
  }

  // 2. Måltider
  const eaters =
    input.eaters && input.eaters.length > 0 ? input.eaters : [{ userId, portionFraction: 1 }];
  const mealIds: string[] = [];
  for (const eater of eaters) {
    const nutrition = scaleNutrition(recipe.nutritionPerPortion, eater.portionFraction);
    const [meal] = await app.db
      .insert(schema.meals)
      .values({
        userId: eater.userId,
        householdId,
        cookingSessionId: session.id,
        date,
        mealType,
        source: "cooked_recipe",
        recipeId: session.recipeId,
        titleSv: recipe.titleSv,
        portionFraction: eater.portionFraction,
        nutrition,
        nutritionIsEstimate: false,
      })
      .returning();
    mealIds.push(meal!.id);
    await emitEvent(app.db, {
      type: "MEAL_LOGGED",
      payload: { mealId: meal!.id, mealType, kcal: nutrition.kcal, source: "cooked_recipe" },
      userId: eater.userId,
      householdId,
      correlationId,
    });
  }

  // 3. Matlådor (Fas 3d): alla rester hamnar i befintliga eller nya meal_boxes.
  let mealBoxId: string | null = null;
  const mealBoxMutations: Array<{ mealBoxId: string; deltaPortions: number; frozen: boolean }> = [];
  const leftoverEstimatePortionsForBox = input.leftoverEstimatePortions ?? 0;
  if (leftoverEstimatePortionsForBox > 0) {
    const locationId =
      input.mealBoxStorageLocationId ??
      (
        await app.db
          .select({ id: schema.storageLocations.id })
          .from(schema.storageLocations)
          .where(
            and(
              eq(schema.storageLocations.householdId, householdId),
              eq(schema.storageLocations.type, input.mealBoxFrozen ? "freezer" : "fridge"),
            ),
          )
          .limit(1)
      )[0]?.id;
    if (!locationId) throw errors.badRequest("Ingen förvaringsplats för matlådor hittades.");

    const frozen = input.mealBoxFrozen ?? false;
    const useByDays = frozen ? 90 : 3;
    const recommendedUseBy = new Date(Date.parse(date) + useByDays * 86_400_000)
      .toISOString()
      .slice(0, 10);

    const [existingBox] = await app.db
      .select()
      .from(schema.mealBoxes)
      .where(
        and(
          eq(schema.mealBoxes.householdId, householdId),
          eq(schema.mealBoxes.recipeId, session.recipeId),
          eq(schema.mealBoxes.cookedAt, date),
          eq(schema.mealBoxes.frozen, frozen),
          eq(schema.mealBoxes.status, "available"),
        ),
      )
      .orderBy(asc(schema.mealBoxes.recommendedUseBy))
      .limit(1);

    if (existingBox) {
      const newRecommendedUseBy =
        existingBox.recommendedUseBy < recommendedUseBy
          ? existingBox.recommendedUseBy
          : recommendedUseBy;
      await app.db
        .update(schema.mealBoxes)
        .set({
          portions: existingBox.portions + leftoverEstimatePortionsForBox,
          portionsRemaining: existingBox.portionsRemaining + leftoverEstimatePortionsForBox,
          recommendedUseBy: newRecommendedUseBy,
          leftoverSource: "cook_session",
        })
        .where(eq(schema.mealBoxes.id, existingBox.id));
      mealBoxId = existingBox.id;
      mealBoxMutations.push({
        mealBoxId: existingBox.id,
        deltaPortions: leftoverEstimatePortionsForBox,
        frozen,
      });
      await emitEvent(app.db, {
        type: "MEAL_BOX_UPDATED",
        payload: {
          mealBoxId: existingBox.id,
          addedPortions: leftoverEstimatePortionsForBox,
          totalPortions: existingBox.portions + leftoverEstimatePortionsForBox,
        },
        userId,
        householdId,
        correlationId,
      });
    } else {
      const [box] = await app.db
        .insert(schema.mealBoxes)
        .values({
          householdId,
          recipeId: session.recipeId,
          cookingSessionId: session.id,
          titleSv: recipe.titleSv,
          portions: leftoverEstimatePortionsForBox,
          portionsRemaining: leftoverEstimatePortionsForBox,
          nutritionPerPortion: recipe.nutritionPerPortion,
          cookedAt: date,
          storageLocationId: locationId,
          frozen,
          recommendedUseBy,
          leftoverSource: "cook_session",
        })
        .returning();
      mealBoxId = box!.id;
      mealBoxMutations.push({
        mealBoxId: box!.id,
        deltaPortions: leftoverEstimatePortionsForBox,
        frozen,
      });
      await emitEvent(app.db, {
        type: "MEAL_BOX_CREATED",
        payload: { mealBoxId: box!.id, portions: leftoverEstimatePortionsForBox },
        userId,
        householdId,
        correlationId,
      });
    }

    await trackProductAnalytics(
      app.db,
      userId,
      leftoversCreated({
        householdId,
        properties: {
          cookingSessionId: session.id,
          recipeId: session.recipeId,
          portions: leftoverEstimatePortionsForBox,
          mealBoxId: mealBoxId!,
          merged: !!existingBox,
        },
      }),
    );
  }

  // 4. recipe_cooks + statistik
  await app.db.insert(schema.recipeCooks).values({
    recipeId: session.recipeId,
    userId,
    householdId,
    cookingSessionId: session.id,
    portionsCooked,
    cookedAt: new Date(),
  });
  await app.db
    .update(schema.recipes)
    .set({ cookCount: sql`${schema.recipes.cookCount} + 1` })
    .where(eq(schema.recipes.id, session.recipeId));
  await emitEvent(app.db, {
    type: "RECIPE_COOKED",
    payload: {
      recipeId: session.recipeId,
      portions: portionsCooked,
      mealBoxPortions: input.mealBoxPortions ?? 0,
    },
    userId,
    householdId,
    correlationId,
  });

  // 5. Uppdatera session (svaren skrivs över av completeCookingSession).
  await app.db
    .update(schema.cookingSessions)
    .set({
      status: "completed",
      completedAt: new Date(),
      plannedDeductions: deductions,
      updatedAt: new Date(),
    })
    .where(eq(schema.cookingSessions.id, session.id));

  await markMilestone(app.db, householdId, "firstCookingSessionCompletedAt");
  if (deductions.length > 0) {
    await markMilestone(app.db, householdId, "inventoryUpdatedAfterCookingAt");
  }

  return {
    ok: true,
    mealIds,
    mealBoxId,
    mealBoxMutations,
    inventoryDeductions: deductions,
    recipeIngredients: recipe.ingredients.map((ing) => ({
      canonicalIngredientId: ing.canonicalIngredientId,
      displayName: ing.displayNameSv,
      quantity: ing.quantity,
      unit: ing.unit,
      optional: ing.optional,
    })),
  };
}

export interface UndoCookingSessionResult {
  ok: boolean;
  reversedTransactions: number;
  restoredItemIds: string[];
  removedMealIds: string[];
  discardedMealBoxIds: string[];
}

/**
 * Ångra en completed cooking session (Fas 3 §6.3).
 *
 * - Ledger är append-only: befintliga transaktioner rörs inte; istället skrivs
 *   reversal-transaktioner (type='correction') med motsatt delta.
 * - Inventory-saldo räknas om från transaktionshistoriken; depletedAt nollas där
 *   saldot blir > 0 igen.
 * - meals och meal_boxes som skapades av sessionen tas bort resp. markeras
 *   discarded; recipe_cooks-raden tas bort och cookCount backas.
 * - Aktiveringsmilstolpar rörs inte (once-ever).
 * - Antagandeprofiler rullas tillbaka deterministiskt via lastSessionAnswers.
 * - undo_until = completedAt + 24 h, verkställt server-side i UTC.
 */
export async function undoCookingSession(
  app: FastifyInstance,
  session: typeof schema.cookingSessions.$inferSelect,
  userId: string,
  correlationId: string,
): Promise<UndoCookingSessionResult> {
  if (session.status !== "completed") {
    throw errors.conflict("Sessionen måste vara avslutad för att kunna ångras.");
  }
  if (!session.completedAt) {
    throw errors.internal("Sessionen saknar completedAt.");
  }

  const now = new Date();
  const deadline = new Date(session.completedAt.getTime() + 24 * 60 * 60 * 1000);
  if (now > deadline) {
    const languageTag = await userLanguageTag(app.db, userId);
    throw errors.conflict(t("cooked.undoWindowExpired", languageTag));
  }

  // 1. Reversera inventory-transaktioner (append-only).
  const cookUses = await app.db
    .select()
    .from(schema.inventoryTransactions)
    .where(
      and(
        eq(schema.inventoryTransactions.cookingSessionId, session.id),
        eq(schema.inventoryTransactions.type, "cook_use"),
      ),
    );

  const affectedItemIds = new Set<string>();
  for (const tx of cookUses) {
    await app.db.insert(schema.inventoryTransactions).values({
      householdId: tx.householdId,
      inventoryItemId: tx.inventoryItemId,
      cookingSessionId: session.id,
      type: "correction",
      quantityDelta: -tx.quantityDelta,
      unit: tx.unit,
      refType: "cooking_session_undo",
      refId: session.id,
      actorUserId: userId,
      note: "Ångrat cook_use",
    });
    affectedItemIds.add(tx.inventoryItemId);
  }

  // 2. Återställ inventory-saldon från transaktionerna.
  for (const itemId of affectedItemIds) {
    const txs = await app.db
      .select({
        type: schema.inventoryTransactions.type,
        quantityDelta: schema.inventoryTransactions.quantityDelta,
        unit: schema.inventoryTransactions.unit,
      })
      .from(schema.inventoryTransactions)
      .where(eq(schema.inventoryTransactions.inventoryItemId, itemId));
    const balance = computeBalance(txs);
    await app.db
      .update(schema.inventoryItems)
      .set({
        quantity: balance.balance,
        depletedAt: balance.balance <= 1e-9 ? new Date() : null,
        updatedAt: new Date(),
      })
      .where(eq(schema.inventoryItems.id, itemId));
  }

  // 3. Ta bort måltider.
  const mealsToRemove = await app.db
    .select()
    .from(schema.meals)
    .where(eq(schema.meals.cookingSessionId, session.id));
  for (const meal of mealsToRemove) {
    await emitEvent(app.db, {
      type: "MEAL_REMOVED",
      payload: { mealId: meal.id, recipeId: session.recipeId, source: "cooking_session_undo" },
      userId: meal.userId,
      householdId: session.householdId,
      correlationId,
    });
  }
  await app.db.delete(schema.meals).where(eq(schema.meals.cookingSessionId, session.id));

  // 4. Återför matlådeportioner (3d). Mutationerna lagrade på sessionen låter
  // oss backa även när rester slagits ihop med en befintlig matlåda.
  const discardedMealBoxIds: string[] = [];
  const mutations = session.mealBoxMutations ?? [];
  if (mutations.length > 0) {
    for (const mutation of mutations) {
      const [box] = await app.db
        .select()
        .from(schema.mealBoxes)
        .where(eq(schema.mealBoxes.id, mutation.mealBoxId))
        .limit(1);
      if (!box) continue;

      const newPortions = Math.max(0, box.portions - mutation.deltaPortions);
      const newRemaining = Math.max(0, box.portionsRemaining - mutation.deltaPortions);
      if (newPortions <= 0) {
        await app.db
          .update(schema.mealBoxes)
          .set({ status: "discarded", portionsRemaining: 0 })
          .where(eq(schema.mealBoxes.id, box.id));
        discardedMealBoxIds.push(box.id);
        await emitEvent(app.db, {
          type: "MEAL_BOX_DISCARDED",
          payload: {
            mealBoxId: box.id,
            portions: mutation.deltaPortions,
            source: "cooking_session_undo",
          },
          userId,
          householdId: session.householdId,
          correlationId,
        });
      } else {
        await app.db
          .update(schema.mealBoxes)
          .set({ portions: newPortions, portionsRemaining: newRemaining })
          .where(eq(schema.mealBoxes.id, box.id));
        await emitEvent(app.db, {
          type: "MEAL_BOX_UPDATED",
          payload: {
            mealBoxId: box.id,
            addedPortions: -mutation.deltaPortions,
            totalPortions: newPortions,
          },
          userId,
          householdId: session.householdId,
          correlationId,
        });
      }
    }
  } else {
    // Legacy: före 3d lagrades cookingSessionId på lådan.
    const boxesToDiscard = await app.db
      .select()
      .from(schema.mealBoxes)
      .where(eq(schema.mealBoxes.cookingSessionId, session.id));
    for (const box of boxesToDiscard) {
      await emitEvent(app.db, {
        type: "MEAL_BOX_DISCARDED",
        payload: { mealBoxId: box.id, portions: box.portions, source: "cooking_session_undo" },
        userId,
        householdId: session.householdId,
        correlationId,
      });
      discardedMealBoxIds.push(box.id);
    }
    await app.db
      .update(schema.mealBoxes)
      .set({ status: "discarded", portionsRemaining: 0 })
      .where(eq(schema.mealBoxes.cookingSessionId, session.id));
  }

  // 5. Ta bort recipe_cooks-raden och backa cookCount.
  await app.db
    .delete(schema.recipeCooks)
    .where(eq(schema.recipeCooks.cookingSessionId, session.id));
  await app.db
    .update(schema.recipes)
    .set({ cookCount: sql`GREATEST(${schema.recipes.cookCount} - 1, 0)` })
    .where(eq(schema.recipes.id, session.recipeId));

  // 6. Rulla tillbaka antagandeprofiler.
  const recipe = await loadFullRecipe(app, session.recipeId);
  for (const ing of recipe.ingredients) {
    if (ing.optional) continue;
    const [existing] = await app.db
      .select()
      .from(schema.cookingAssumptionProfiles)
      .where(
        and(
          eq(schema.cookingAssumptionProfiles.householdId, session.householdId),
          eq(schema.cookingAssumptionProfiles.canonicalIngredientId, ing.canonicalIngredientId),
        ),
      )
      .limit(1);
    if (!existing) continue;

    const rolledBack = rollbackCookingAssumptionProfile(session.id, {
      averageEatenPortions: existing.averageEatenPortions,
      averageLeftoverPortions: existing.averageLeftoverPortions,
      observationCount: existing.observationCount,
      lastSessionAnswers: existing.lastSessionAnswers,
    });

    if (rolledBack.observationCount === 0) {
      await app.db
        .delete(schema.cookingAssumptionProfiles)
        .where(
          and(
            eq(schema.cookingAssumptionProfiles.householdId, session.householdId),
            eq(schema.cookingAssumptionProfiles.canonicalIngredientId, ing.canonicalIngredientId),
          ),
        );
    } else {
      await app.db
        .insert(schema.cookingAssumptionProfiles)
        .values({
          householdId: session.householdId,
          canonicalIngredientId: ing.canonicalIngredientId,
          averageEatenPortions: rolledBack.averageEatenPortions,
          averageLeftoverPortions: rolledBack.averageLeftoverPortions,
          observationCount: rolledBack.observationCount,
          lastSessionAnswers: rolledBack.lastSessionAnswers,
          updatedAt: new Date(),
        })
        .onConflictDoUpdate({
          target: [
            schema.cookingAssumptionProfiles.householdId,
            schema.cookingAssumptionProfiles.canonicalIngredientId,
          ],
          set: {
            averageEatenPortions: rolledBack.averageEatenPortions,
            averageLeftoverPortions: rolledBack.averageLeftoverPortions,
            observationCount: rolledBack.observationCount,
            lastSessionAnswers: rolledBack.lastSessionAnswers,
            updatedAt: new Date(),
          },
        });
    }
  }

  // 7. Sätt sessionstatus till undone.
  await app.db
    .update(schema.cookingSessions)
    .set({
      status: "undone",
      updatedAt: new Date(),
    })
    .where(eq(schema.cookingSessions.id, session.id));

  await trackProductAnalytics(
    app.db,
    userId,
    cookingSessionUndone({
      householdId: session.householdId,
      properties: {
        cookingSessionId: session.id,
        recipeId: session.recipeId,
        reversedTransactions: cookUses.length,
      },
    }),
  );

  await emitEvent(app.db, {
    type: "COOKING_SESSION_UNDONE",
    payload: {
      cookingSessionId: session.id,
      recipeId: session.recipeId,
      reversedTransactions: cookUses.length,
    },
    userId,
    householdId: session.householdId,
    correlationId,
  });

  return {
    ok: true,
    reversedTransactions: cookUses.length,
    restoredItemIds: [...affectedItemIds],
    removedMealIds: mealsToRemove.map((m) => m.id),
    discardedMealBoxIds,
  };
}
