///|
pub fn ingredient_has_allergen(
  ingredient : Ingredient,
  profile : DietProfile,
) -> Bool {
  count_intersection(ingredient.allergens, profile.avoid_allergens) > 0
}

///|
pub fn allergen_hit_count(recipe : Recipe, profile : DietProfile) -> Int {
  let mut count = 0
  for ingredient in recipe.ingredients {
    if ingredient_has_allergen(ingredient, profile) {
      count += 1
    }
  }
  count
}

///|
pub fn disliked_tag_count(recipe : Recipe, profile : DietProfile) -> Int {
  let mut count = 0
  for ingredient in recipe.ingredients {
    if count_intersection(ingredient.tags, profile.dislike_tags) > 0 {
      count += 1
    }
  }
  count
}

///|
fn pantry_lookup(pantry : Array[PantryItem], name : String) -> PantryItem? {
  for item in pantry {
    if same_name(item.name, name) {
      return Some(item)
    }
  }
  None
}

///|
fn substitution_lookup(
  substitutions : Array[Substitution],
  ingredient : Ingredient,
  profile : DietProfile,
) -> Substitution? {
  for sub in substitutions {
    if same_name(sub.source, ingredient.name) {
      let removes = count_intersection(
        ingredient.allergens,
        profile.avoid_allergens,
      )
      let target_hits = count_intersection(
        sub.target.allergens,
        profile.avoid_allergens,
      )
      if removes > 0 && target_hits == 0 {
        return Some(sub)
      }
    }
  }
  None
}

///|
fn apply_substitution(
  ingredient : Ingredient,
  sub : Substitution,
) -> Ingredient {
  let q = Quantity(
    amount=ingredient.quantity.amount * sub.ratio_percent / 100,
    unit=ingredient.quantity.unit,
  )
  Ingredient(
    name=sub.target.name,
    group=sub.target.group,
    quantity=q,
    nutrition=sub.target.nutrition,
    cost_cents=sub.target.cost_cents,
    allergens=sub.target.allergens,
    tags=sub.target.tags,
  )
}

///|
pub fn adapt_recipe(
  recipe : Recipe,
  profile : DietProfile,
  substitutions : Array[Substitution],
) -> Recipe {
  let adapted = Array::new(capacity=recipe.ingredients.length())
  for ingredient in recipe.ingredients {
    match substitution_lookup(substitutions, ingredient, profile) {
      Some(sub) => adapted.push(apply_substitution(ingredient, sub))
      None => adapted.push(ingredient)
    }
  }
  Recipe(
    title=recipe.title,
    servings=recipe.servings,
    ingredients=adapted,
    steps=recipe.steps,
    notes=recipe.notes,
  )
}

///|
pub fn used_substitutions(
  recipe : Recipe,
  profile : DietProfile,
  substitutions : Array[Substitution],
) -> Array[Substitution] {
  let used = Array::new()
  for ingredient in recipe.ingredients {
    match substitution_lookup(substitutions, ingredient, profile) {
      Some(sub) => used.push(sub)
      None => ()
    }
  }
  used
}

///|
pub fn shopping_list(
  recipe : Recipe,
  pantry : Array[PantryItem],
) -> Array[ShoppingLine] {
  let lines = Array::new(capacity=recipe.ingredients.length())
  for ingredient in recipe.ingredients {
    let available = match pantry_lookup(pantry, ingredient.name) {
      Some(item) => item.quantity
      None => Quantity(amount=0, unit=ingredient.quantity.unit)
    }
    let buy = quantity_missing(ingredient.quantity, available)
    let cost = if ingredient.quantity.amount <= 0 {
      0
    } else {
      ingredient.cost_cents * buy.amount / ingredient.quantity.amount
    }
    lines.push(
      ShoppingLine(
        name=ingredient.name,
        needed=ingredient.quantity,
        available~,
        buy~,
        estimated_cost_cents=cost,
      ),
    )
  }
  lines
}

///|
pub fn missing_count(lines : Array[ShoppingLine]) -> Int {
  let mut count = 0
  for line in lines {
    if line.buy.amount > 0 {
      count += 1
    }
  }
  count
}

///|
pub fn shopping_cost(lines : Array[ShoppingLine]) -> Int {
  let mut total = 0
  for line in lines {
    total += line.estimated_cost_cents
  }
  total
}

///|
fn expiry_warnings(pantry : Array[PantryItem]) -> Array[PantryWarning] {
  let warnings = Array::new()
  for item in pantry {
    if item.expires_in_days <= 0 {
      warnings.push(
        PantryWarning(
          code="expired",
          message=item.name + " has expired.",
          severity="warn",
        ),
      )
    } else if item.expires_in_days <= 2 {
      warnings.push(
        PantryWarning(
          code="use-soon",
          message=item.name + " should be used soon.",
          severity="info",
        ),
      )
    }
  }
  warnings
}

///|
fn diet_warnings(
  recipe : Recipe,
  profile : DietProfile,
) -> Array[PantryWarning] {
  let warnings = Array::new()
  let nutrition = total_nutrition(recipe)
  let cost = total_cost(recipe)
  if profile.max_total_kcal > 0 && nutrition.kcal > profile.max_total_kcal {
    warnings.push(
      PantryWarning(
        code="kcal",
        message="Recipe exceeds kcal target by " +
          (nutrition.kcal - profile.max_total_kcal).to_string() +
          ".",
        severity="warn",
      ),
    )
  }
  if profile.min_total_protein_g > 0 &&
    nutrition.protein_g < profile.min_total_protein_g {
    warnings.push(
      PantryWarning(
        code="protein",
        message="Recipe is below protein target by " +
          (profile.min_total_protein_g - nutrition.protein_g).to_string() +
          "g.",
        severity="info",
      ),
    )
  }
  if profile.max_total_cost_cents > 0 && cost > profile.max_total_cost_cents {
    warnings.push(
      PantryWarning(
        code="cost",
        message="Recipe exceeds cost target by " +
          money_text(cost - profile.max_total_cost_cents) +
          ".",
        severity="warn",
      ),
    )
  }
  warnings
}

///|
fn concat_warnings(
  left : Array[PantryWarning],
  right : Array[PantryWarning],
) -> Array[PantryWarning] {
  let out = Array::new(capacity=left.length() + right.length())
  for item in left {
    out.push(item)
  }
  for item in right {
    out.push(item)
  }
  out
}

///|
pub fn plan_recipe(
  recipe : Recipe,
  servings : Int,
  profile : DietProfile,
  pantry : Array[PantryItem],
  substitutions : Array[Substitution],
) -> PantryPlan {
  let scaled = scale_recipe(recipe, servings)
  let used = used_substitutions(scaled, profile, substitutions)
  let adapted = adapt_recipe(scaled, profile, substitutions)
  let shopping = shopping_list(adapted, pantry)
  let nutrition = total_nutrition(adapted)
  let allergen_hits = allergen_hit_count(adapted, profile)
  let missing = missing_count(shopping)
  let warnings = concat_warnings(
    expiry_warnings(pantry),
    diet_warnings(adapted, profile),
  )
  let verdict = if allergen_hits > 0 {
    "blocked-allergen"
  } else if missing > 0 {
    "shopping-needed"
  } else if warnings.length() > 0 {
    "ready-with-notes"
  } else {
    "ready"
  }
  PantryPlan(
    verdict~,
    recipe_title=adapted.title,
    servings~,
    total_cost_cents=total_cost(adapted),
    total_kcal=nutrition.kcal,
    total_protein_g=nutrition.protein_g,
    allergen_hit_count=allergen_hits,
    missing_count=missing,
    substitution_count=used.length(),
    shopping~,
    substitutions=used,
    warnings~,
  )
}

///|
pub fn plan_score(plan : PantryPlan) -> Int {
  let mut score = 100
  score -= plan.allergen_hit_count * 30
  score -= plan.missing_count * 8
  score -= plan.warnings.length() * 3
  if score < 0 {
    0
  } else {
    score
  }
}