package dev.ulfrx.recipe.shared.domain.model

import kotlinx.serialization.Serializable

// ── Value-typed metadata ──────────────────────────────────────────────────

/**
 * Macros per 100 g (or 100 ml for liquid ingredients). Daily-total aggregation
 * (PLAN-13) reads these from ingredients/products and scales by the canonical
 * quantity at meal time.
 */
@Serializable
public data class NutritionPer100(
    public val kcal: Double,
    public val protein: Double,
    public val fat: Double,
    public val carbs: Double,
)

/**
 * Typical retail pack for an ingredient or a specific product (e.g.
 * `PurchasePack(125.0, "125 g")`). Shopping list rounding uses this to suggest
 * realistic pack-sized purchases instead of raw recipe weights.
 */
@Serializable
public data class PurchasePack(
    public val amount: Double,
    public val label: String,
)

// ── Household-scoped taxonomy ────────────────────────────────────────────
//
// IngredientCategory and MealSlot are user-curated per household: each
// household ships with a Polish default list at creation (Phase 3 server
// concern) and can rename / reorder / extend freely. Both LWW-sync via SyncMeta
// the same way other household data does.

/**
 * A pantry / shopping grouping the household uses (`Pieczywo`, `Nabiał`,
 * `Mięso i ryby`, …). [ordinal] drives display order in the pantry and
 * shopping list. [name] is localized per the household's locale palette.
 */
@Serializable
public data class IngredientCategory(
    public val id: IngredientCategoryId,
    public val sync: SyncMeta,
    public val name: LocalizedString,
    public val ordinal: Int,
)

/**
 * A meal-time slot a household plans into (`Śniadanie`, `Drugie śniadanie`,
 * `Obiad`, `Przekąska`, `Kolacja`, plus anything custom). [ordinal] drives the
 * within-day order in the planner. Slots are referenced by [PlanEntry.slotId].
 *
 * Recipes don't pin themselves to specific slots — slot affinity is a UI-layer
 * match between [Recipe.tags] and [name] localized values, so households can
 * rename slots without invalidating the catalog.
 */
@Serializable
public data class MealSlot(
    public val id: MealSlotId,
    public val sync: SyncMeta,
    public val name: LocalizedString,
    public val ordinal: Int,
)

// ── Catalog entities (server-owned in v1, household-editable later) ──────
//
// Ingredient / Product / Recipe carry SyncMeta so that future client-side
// catalog writes land on the same LWW path as everything else; v1 only reads
// them on the client.

/**
 * A catalog-level ingredient (mąka, oliwa, jajko, …). [pantryUnit] is the
 * canonical unit this ingredient is tracked in for pantry math; recipe lines
 * targeting this ingredient must produce a [Quantity] in the same unit (or one
 * that converts cleanly — pieces ↔ grams via [weightPerPieceG]).
 *
 * [weightPerPieceG] is only meaningful when [pantryUnit] == [MeasurementUnit.PIECE];
 * leaving it null on a piece-tracked ingredient blocks nutrition aggregation.
 * [purchasePack] feeds shopping rounding; [nutritionPer100] feeds daily totals.
 */
@Serializable
public data class Ingredient(
    public val id: IngredientId,
    public val sync: SyncMeta,
    public val name: LocalizedString,
    public val categoryId: IngredientCategoryId,
    public val pantryUnit: MeasurementUnit,
    public val weightPerPieceG: Double? = null,
    public val purchasePack: PurchasePack? = null,
    public val nutritionPer100: NutritionPer100? = null,
    public val imagePath: String? = null,
)

/**
 * A branded / packaged variant of an [Ingredient]. Products carry their own
 * [nutritionPer100] because brand A's twaróg has different macros than brand
 * B's, and per-entry [PlanCustomization.overrides] can pick a specific product.
 * [brand] is plain `String?` — brand names are proper nouns and don't translate.
 */
@Serializable
public data class Product(
    public val id: ProductId,
    public val sync: SyncMeta,
    public val ingredientId: IngredientId,
    public val name: LocalizedString,
    public val brand: String? = null,
    public val pack: PurchasePack,
    public val nutritionPer100: NutritionPer100? = null,
    public val imagePath: String? = null,
)

/**
 * One ingredient line on a recipe. [alternatives] is metadata only — the
 * planner UI surfaces these as suggested swaps, but the actual per-entry
 * substitution is recorded in [IngredientCustomization.substituteWith].
 */
@Serializable
public data class RecipeIngredient(
    public val ingredientId: IngredientId,
    public val quantity: Quantity,
    public val alternatives: List<IngredientId> = emptyList(),
)

/**
 * Catalog-level recipe definition. Synced via the same LWW path as household
 * data so future client-side recipe editing lands additively. Recipes are
 * server-seeded in v1 and read-only on the client.
 *
 * No `allowedSlots` field — slot affinity is a UI-layer concern using [tags]
 * matched against [MealSlot.name] localized values. Recipes ship with Polish
 * slot tags so default households work out of the box.
 *
 * [nutritionPerServing] is the cached pre-customization total; the calculator
 * skips the per-ingredient walk when no [PlanCustomization] is present.
 */
@Serializable
public data class Recipe(
    public val id: RecipeId,
    public val sync: SyncMeta,
    public val title: LocalizedString,
    public val minutes: Int,
    public val tags: List<String> = emptyList(),
    public val steps: List<LocalizedString> = emptyList(),
    public val ingredients: List<RecipeIngredient>,
    public val nutritionPerServing: NutritionPer100,
    public val imagePath: String? = null,
)
