import { type DateTime, getWeekStart, toDateOnly } from "@batch-cooking/date-tools";
import type { CookingTaskIngredientView, OptimizedCookingPlanView } from "@batch-cooking/shared";
import type { Prisma } from "@prisma/client";
import { prisma } from "../../db/prisma.js";
import {
  type OptimizerRecipeInput,
  type OptimizerStepInput,
  optimizeCookingPlan,
} from "../../lib/recipe-matching/cooking-optimizer.js";
import { toIngredientView, toUnitView } from "../recipe/recipe.service.js";

/**
 * Prisma `include` for a `Planning` query that needs, for every item, its
 * recipe's ordered steps with the full detected-technique tree — the raw
 * material the optimizer works on (see `cooking-optimizer.ts`). It's the
 * `steps` sub-tree of `recipe.service.ts`'s own `recipeInclude`, resolved
 * the same way so {@link toIngredientView}/{@link toUnitView} can be reused
 * as-is; deliberately narrower than a full `RecipeView` fetch (no
 * diets/favorites/recipe-level ingredient list — the optimizer reads
 * quantities off the technique clauses, not the recipe header).
 */
function cookingSessionPlanningInclude() {
  return {
    items: {
      include: {
        recipe: {
          select: {
            id: true,
            name: true,
            portions: true,
            steps: {
              orderBy: { order: "asc" },
              include: {
                techSteps: {
                  orderBy: { order: "asc" },
                  include: {
                    techStep: true,
                    ingredients: {
                      include: {
                        ingredient: {
                          include: {
                            allergies: { include: { allergy: { include: { category: true } } } },
                            diets: { include: { diet: true } },
                          },
                        },
                        unit: true,
                      },
                    },
                    utensils: { include: { utensil: true } },
                  },
                },
              },
            },
          },
        },
      },
    },
  } satisfies Prisma.PlanningInclude;
}

type PlanningWithSteps = Prisma.PlanningGetPayload<{
  include: ReturnType<typeof cookingSessionPlanningInclude>;
}>;
type PlanningItemWithSteps = PlanningWithSteps["items"][number];

/**
 * Maps one planning item's recipe (with {@link cookingSessionPlanningInclude})
 * to the optimizer's pure input shape — ingredient/unit/technique/utensil
 * rows resolved to their reference views here so the optimizer itself never
 * touches Prisma. `Decimal` quantities become plain numbers (same
 * `Number(...)` conversion as `recipe.service.ts`'s own view mappers); an
 * unresolved-unit line keeps `unit: null`.
 */
function toOptimizerRecipe(item: PlanningItemWithSteps): OptimizerRecipeInput {
  const steps: OptimizerStepInput[] = item.recipe.steps.map((step) => ({
    stepId: step.id,
    order: step.order,
    description: step.description,
    techSteps: step.techSteps.map((techStep) => {
      const ingredients: CookingTaskIngredientView[] = techStep.ingredients.map((line) => ({
        ingredient: toIngredientView(line.ingredient),
        quantity: line.quantity === null ? null : Number(line.quantity),
        unit: line.unit === null ? null : toUnitView(line.unit),
      }));
      return {
        techStep: { id: techStep.techStep.id, key: techStep.techStep.key },
        order: techStep.order,
        ingredients,
        utensils: techStep.utensils.map(({ utensil }) => ({ id: utensil.id, key: utensil.key })),
      };
    }),
  }));

  return {
    recipeId: item.recipe.id,
    name: item.recipe.name,
    // The slot's own portion count vs. the recipe's as-written yield — the
    // optimizer scales technique-clause quantities by the ratio, same
    // reasoning as `shopping-list.service.ts`'s `aggregateShoppingList`.
    portions: item.portions,
    recipePortions: item.recipe.portions,
    steps,
  };
}

/**
 * Builds the household's optimized cooking plan for the week covering
 * `date` — every recipe planned that week, reorganized into ordered phases
 * that pool shared prep and float passive cooks into the background (see
 * `cooking-optimizer.ts`). `date` follows the same convention as
 * `planning.service.ts`'s `getPlanningForDate` (a caller-parsed `?date=`,
 * not necessarily a Monday).
 *
 * Like `getShoppingListForDate` and unlike `getPlanningForDate`, this
 * **never** returns `null` — no household and "no planning covers this week
 * yet" both degrade to an empty `phases`/`recipes` on an otherwise normal
 * {@link OptimizedCookingPlanView} (the week's date range is always
 * computable from `date` alone).
 */
export async function getCookingPlanForDate(
  houseId: number | null,
  date: DateTime,
): Promise<OptimizedCookingPlanView> {
  try {
    const weekStart = getWeekStart(toDateOnly(date));
    const weekFinish = weekStart.plus({ days: 6 });
    const emptyPlan: OptimizedCookingPlanView = {
      startDate: weekStart.toJSDate().toISOString(),
      finishDate: weekFinish.toJSDate().toISOString(),
      recipes: [],
      phases: [],
    };

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

    // Same "covering range" lookup as getShoppingListForDate — see
    // getPlanningForDate's doc comment for the UTC-midnight `Date` rationale.
    const dateOnly = toDateOnly(date).toJSDate();
    const planning = await prisma.planning.findFirst({
      where: {
        houseId,
        startDate: { lte: dateOnly },
        finishDate: { gte: dateOnly },
      },
      orderBy: { startDate: "desc" },
      include: cookingSessionPlanningInclude(),
    });

    if (!planning) {
      return emptyPlan;
    }

    const { recipes, phases } = optimizeCookingPlan(planning.items.map(toOptimizerRecipe));
    return {
      startDate: planning.startDate.toISOString(),
      finishDate: planning.finishDate.toISOString(),
      recipes,
      phases,
    };
  } catch (err) {
    // Rethrown as-is — `wrapAsyncHandler`/the error middleware handles it,
    // this service layer just isn't allowed a bare `await` per the repo's
    // async/try-catch convention.
    throw err;
  }
}
