///|
pub(all) struct MealPrepOption {
  recipe_title : String
  servings : Int
  verdict : String
  plan_score : Int
  rotation_score : Int
  expiring_used_count : Int
  expiring_unused_count : Int
  shopping_cost_cents : Int
  total_protein_g : Int
  score : Int
  reason : String
} derive(Eq, Debug)

///|
pub fn MealPrepOption::MealPrepOption(
  recipe_title? : String = "",
  servings? : Int = 0,
  verdict? : String = "",
  plan_score? : Int = 0,
  rotation_score? : Int = 0,
  expiring_used_count? : Int = 0,
  expiring_unused_count? : Int = 0,
  shopping_cost_cents? : Int = 0,
  total_protein_g? : Int = 0,
  score? : Int = 0,
  reason? : String = "",
) -> MealPrepOption {
  {
    recipe_title,
    servings,
    verdict,
    plan_score,
    rotation_score,
    expiring_used_count,
    expiring_unused_count,
    shopping_cost_cents,
    total_protein_g,
    score,
    reason,
  }
}

///|
pub(all) struct MealPrepPlan {
  days : Int
  window_days : Int
  options : Array[MealPrepOption]
  best : MealPrepOption
  verdict : String
  score : Int
} derive(Eq, Debug)

///|
pub fn MealPrepPlan::MealPrepPlan(
  days? : Int = 0,
  window_days? : Int = 0,
  options? : Array[MealPrepOption] = [],
  best? : MealPrepOption = MealPrepOption(),
  verdict? : String = "",
  score? : Int = 0,
) -> MealPrepPlan {
  { days, window_days, options, best, verdict, score }
}

///|
fn recipe_uses_pantry_item(recipe : Recipe, item : PantryItem) -> Bool {
  for ingredient in recipe.ingredients {
    if same_name(ingredient.name, item.name) {
      return true
    }
  }
  false
}

///|
fn expiring_used_by_recipe(
  recipe : Recipe,
  pantry : Array[PantryItem],
  window_days : Int,
) -> Int {
  let mut count = 0
  for item in pantry {
    if item.expires_in_days <= window_days &&
      recipe_uses_pantry_item(recipe, item) {
      count += 1
    }
  }
  count
}

///|
fn meal_prep_reason(option : MealPrepOption) -> String {
  if option.verdict == "blocked-allergen" {
    return "blocked because allergens remain after substitutions"
  }
  if option.expiring_used_count > 0 && option.shopping_cost_cents == 0 {
    return "uses expiring pantry stock without extra shopping"
  }
  if option.expiring_used_count > 0 {
    return "uses expiring pantry stock and keeps shopping gaps explicit"
  }
  if option.shopping_cost_cents == 0 {
    return "ready from pantry stock"
  }
  "valid meal option with shopping gaps"
}

///|
fn clamp_meal_prep_score(score : Int) -> Int {
  if score < 0 {
    0
  } else if score > 150 {
    150
  } else {
    score
  }
}

///|
pub fn meal_prep_option(
  recipe : Recipe,
  servings : Int,
  profile : DietProfile,
  pantry : Array[PantryItem],
  substitutions : Array[Substitution],
  window_days : Int,
) -> MealPrepOption {
  let scaled = scale_recipe(recipe, servings)
  let adapted = adapt_recipe(scaled, profile, substitutions)
  let plan = plan_recipe(recipe, servings, profile, pantry, substitutions)
  let rotation = rotation_plan(adapted, pantry)
  let used = expiring_used_by_recipe(adapted, pantry, window_days)
  let unused = expiring_not_in_recipe(adapted, pantry, window_days).length()
  let base = plan_score(plan)
  let waste_bonus = used * 14
  let rotation_bonus = rotation_score(rotation) / 12
  let unused_penalty = unused * 8
  let shopping_penalty = shopping_cost(plan.shopping) / 600
  let allergen_penalty = plan.allergen_hit_count * 40
  let score = clamp_meal_prep_score(
    base +
    waste_bonus +
    rotation_bonus -
    unused_penalty -
    shopping_penalty -
    allergen_penalty,
  )
  let draft = MealPrepOption(
    recipe_title=plan.recipe_title,
    servings=plan.servings,
    verdict=plan.verdict,
    plan_score=base,
    rotation_score=rotation_score(rotation),
    expiring_used_count=used,
    expiring_unused_count=unused,
    shopping_cost_cents=shopping_cost(plan.shopping),
    total_protein_g=plan.total_protein_g,
    score~,
  )
  MealPrepOption(
    recipe_title=draft.recipe_title,
    servings=draft.servings,
    verdict=draft.verdict,
    plan_score=draft.plan_score,
    rotation_score=draft.rotation_score,
    expiring_used_count=draft.expiring_used_count,
    expiring_unused_count=draft.expiring_unused_count,
    shopping_cost_cents=draft.shopping_cost_cents,
    total_protein_g=draft.total_protein_g,
    score=draft.score,
    reason=meal_prep_reason(draft),
  )
}

///|
pub fn meal_prep_rankings(
  recipes : Array[Recipe],
  servings : Int,
  profile : DietProfile,
  pantry : Array[PantryItem],
  substitutions : Array[Substitution],
  window_days : Int,
) -> Array[MealPrepOption] {
  let options = Array::new(capacity=recipes.length())
  for recipe in recipes {
    options.push(
      meal_prep_option(
        recipe, servings, profile, pantry, substitutions, window_days,
      ),
    )
  }
  options
}

///|
pub fn best_meal_prep_option(options : Array[MealPrepOption]) -> MealPrepOption {
  if options.length() == 0 {
    return MealPrepOption(verdict="empty")
  }
  let mut best = options[0]
  for option in options {
    if option.score > best.score {
      best = option
    } else if option.score == best.score &&
      option.expiring_used_count > best.expiring_used_count {
      best = option
    } else if option.score == best.score &&
      option.expiring_used_count == best.expiring_used_count &&
      option.shopping_cost_cents < best.shopping_cost_cents {
      best = option
    }
  }
  best
}

///|
pub fn zero_waste_meal_prep(
  recipes : Array[Recipe],
  servings : Int,
  profile : DietProfile,
  pantry : Array[PantryItem],
  substitutions : Array[Substitution],
  days : Int,
  window_days : Int,
) -> MealPrepPlan {
  let options = meal_prep_rankings(
    recipes, servings, profile, pantry, substitutions, window_days,
  )
  let best = best_meal_prep_option(options)
  let verdict = if options.length() == 0 {
    "empty"
  } else if best.verdict == "blocked-allergen" {
    "needs-safe-recipe"
  } else if best.expiring_used_count > 0 {
    "zero-waste-priority"
  } else {
    "pantry-balanced"
  }
  MealPrepPlan(days~, window_days~, options~, best~, verdict~, score=best.score)
}

///|
pub fn MealPrepPlan::summary(self : MealPrepPlan) -> String {
  "mealprep=" +
  self.verdict +
  " days=" +
  self.days.to_string() +
  " window=" +
  self.window_days.to_string() +
  " options=" +
  self.options.length().to_string() +
  " best=\"" +
  self.best.recipe_title +
  "\" score=" +
  self.score.to_string() +
  " expiring-used=" +
  self.best.expiring_used_count.to_string()
}

///|
pub fn MealPrepPlan::to_markdown(self : MealPrepPlan) -> String {
  let out = StringBuilder()
  out.write_string("# PantryCalc Zero-Waste Meal Prep\n\n")
  out.write_string("- Verdict: " + self.verdict + "\n")
  out.write_string("- Days: " + self.days.to_string() + "\n")
  out.write_string("- Expiry window: " + self.window_days.to_string() + "\n")
  out.write_string("- Best recipe: " + self.best.recipe_title + "\n")
  out.write_string("- Best score: " + self.score.to_string() + "\n\n")
  out.write_string("## Ranked Options\n\n")
  if self.options.length() == 0 {
    out.write_string("- No recipes provided.\n")
  }
  for option in self.options {
    out.write_string(
      "- " +
      option.recipe_title +
      ": score " +
      option.score.to_string() +
      ", verdict " +
      option.verdict +
      ", expiring used " +
      option.expiring_used_count.to_string() +
      ", shopping " +
      money_text(option.shopping_cost_cents) +
      ", " +
      option.reason +
      "\n",
    )
  }
  out.to_string()
}

///|
pub fn example_zero_waste_meal_prep() -> MealPrepPlan {
  zero_waste_meal_prep(
    [example_recipe(), pantry_rice_bowl()],
    4,
    example_profile(),
    example_pantry(),
    default_substitutions(),
    3,
    2,
  )
}