import { type DateTime, getWeekStart, toDateOnly } from "@batch-cooking/date-tools";
import type {
  IngredientView,
  ShoppingListItemView,
  ShoppingListView,
  UnitView,
} from "@batch-cooking/shared";
import type { Prisma } from "@prisma/client";
import { prisma } from "../../db/prisma.js";
import { toIngredientView, toUnitView } from "../recipe/recipe.service.js";

/** Prisma `include` for a `Planning` query that needs, for every item, just enough of its recipe to compute a shopping list — `portions` (to scale `RecipeIngredient.quantity`) and the ingredient lines themselves, each resolved the same way `recipe.service.ts`'s own `recipeInclude` resolves them (so {@link toIngredientView}/{@link toUnitView} can be reused as-is). Deliberately narrower than a full `RecipeView` fetch — steps/diets/favorites are never read here. */
function shoppingListPlanningInclude() {
  return {
    items: {
      include: {
        recipe: {
          select: {
            portions: true,
            ingredients: {
              include: {
                ingredient: {
                  include: {
                    allergies: { include: { allergy: { include: { category: true } } } },
                    diets: { include: { diet: true } },
                  },
                },
                unit: true,
              },
            },
          },
        },
      },
    },
  } satisfies Prisma.PlanningInclude;
}

type PlanningWithIngredients = Prisma.PlanningGetPayload<{
  include: ReturnType<typeof shoppingListPlanningInclude>;
}>;

/** Accumulates a running sum per `(ingredientId, unitId)` pair while walking every planning item's ingredient lines — see {@link aggregateShoppingList}. */
interface RunningTotal {
  ingredient: IngredientView;
  unit: UnitView;
  quantity: number;
}

/**
 * Sums every ingredient line across `items`, each scaled by that planning
 * item's own portion count relative to its recipe's as-written yield
 * (`RecipeIngredient.quantity × PlanningItem.portions / Recipe.portions`,
 * see `PlanningItem.portions`'s doc comment in schema.prisma for why the
 * two can differ). Grouped by `(ingredientId, unitId)` — **not** just
 * `ingredientId` — since summing across units isn't implemented yet (see
 * `ShoppingListItemView`'s doc comment): the same ingredient requested in
 * two different units stays two separate lines rather than silently
 * guessing a conversion. Pure/synchronous, factored out from
 * {@link getShoppingListForDate} so the aggregation itself is testable
 * without a database round-trip.
 */
function aggregateShoppingList(items: PlanningWithIngredients["items"]): ShoppingListItemView[] {
  const totals = new Map<string, RunningTotal>();

  for (const item of items) {
    const scale = item.portions / item.recipe.portions;
    for (const recipeIngredient of item.recipe.ingredients) {
      const key = `${recipeIngredient.ingredientId}:${recipeIngredient.unitId}`;
      const addedQuantity = Number(recipeIngredient.quantity) * scale;

      const existing = totals.get(key);
      if (existing) {
        existing.quantity += addedQuantity;
      } else {
        totals.set(key, {
          ingredient: toIngredientView(recipeIngredient.ingredient),
          unit: toUnitView(recipeIngredient.unit),
          quantity: addedQuantity,
        });
      }
    }
  }

  // Deterministic order (by the ingredient's stable `key`, not its id —
  // insertion order would otherwise depend on which recipe happened to be
  // read first) — the frontend re-sorts by translated label/aisle for
  // display, this is just so two identical plannings always produce the
  // same JSON.
  return [...totals.values()].sort((a, b) => a.ingredient.key.localeCompare(b.ingredient.key));
}

/**
 * Builds the household's shopping list for the week covering `date` —
 * every ingredient line of every recipe planned that week, aggregated (see
 * {@link aggregateShoppingList}). `date` is whatever the caller wants "that
 * week" to mean, same convention as `planning.service.ts`'s
 * `getPlanningForDate` (a caller-parsed `?date=`, not necessarily a
 * Monday).
 *
 * Unlike `getPlanningForDate`, this **never** returns `null` — no household
 * and "no planning covers this week yet" both degrade to an empty `items`
 * array on an otherwise normal `ShoppingListView` (the week's date range is
 * always computable from `date` alone, even with nothing planned in it),
 * rather than a separate "nothing to show" state the frontend would have to
 * branch on.
 */
export async function getShoppingListForDate(
  houseId: number | null,
  date: DateTime,
): Promise<ShoppingListView> {
  try {
    const weekStart = getWeekStart(toDateOnly(date));
    const weekFinish = weekStart.plus({ days: 6 });
    const emptyList: ShoppingListView = {
      startDate: weekStart.toJSDate().toISOString(),
      finishDate: weekFinish.toJSDate().toISOString(),
      items: [],
    };

    if (houseId === null) {
      return emptyList;
    }

    // Same "covering range" lookup as getPlanningForDate — see that
    // function's doc comment for why this compares against a UTC-midnight
    // JS Date rather than `weekStart`/`weekFinish` directly.
    const dateOnly = toDateOnly(date).toJSDate();
    const planning = await prisma.planning.findFirst({
      where: {
        houseId,
        startDate: { lte: dateOnly },
        finishDate: { gte: dateOnly },
      },
      orderBy: { startDate: "desc" },
      include: shoppingListPlanningInclude(),
    });

    if (!planning) {
      return emptyList;
    }

    return {
      startDate: planning.startDate.toISOString(),
      finishDate: planning.finishDate.toISOString(),
      items: aggregateShoppingList(planning.items),
    };
  } catch (err) {
    // Rethrown as-is — `wrapAsyncHandler`/the error middleware (which
    // already logs it, see `error-logger.ts`) is what actually handles it,
    // this service layer just isn't allowed a bare `await` per the repo's
    // async/try-catch convention.
    throw err;
  }
}
