///|
pub(all) struct RotationLine {
  name : String
  required : Quantity
  available : Quantity
  use_first : Quantity
  buy : Quantity
  priority : Int
  reason : String
} derive(Eq, Debug)

///|
pub fn RotationLine::RotationLine(
  name? : String = "",
  required? : Quantity = Quantity(),
  available? : Quantity = Quantity(),
  use_first? : Quantity = Quantity(),
  buy? : Quantity = Quantity(),
  priority? : Int = 0,
  reason? : String = "",
) -> RotationLine {
  { name, required, available, use_first, buy, priority, reason }
}

///|
fn min_quantity(left : Quantity, right : Quantity) -> Quantity {
  if !same_measure_kind(left, right) {
    return left
  }
  let left_base = canonical_amount(left)
  let right_base = canonical_amount(right)
  if left_base <= right_base {
    left
  } else {
    convert_quantity(right, left.unit)
  }
}

///|
fn rotation_priority(item : PantryItem) -> Int {
  let mut score = 0
  if item.expires_in_days <= 0 {
    score += 100
  } else if item.expires_in_days <= 2 {
    score += 70
  } else if item.expires_in_days <= 7 {
    score += 40
  } else if item.expires_in_days <= 14 {
    score += 20
  }
  if item.opened {
    score += 15
  }
  if item.preferred {
    score += 5
  }
  score
}

///|
fn rotation_reason(item : PantryItem) -> String {
  if item.expires_in_days <= 0 {
    return "expired stock should be removed before cooking"
  }
  if item.expires_in_days <= 2 {
    return "use immediately because it expires within two days"
  }
  if item.expires_in_days <= 7 && item.opened {
    return "opened item expires this week"
  }
  if item.expires_in_days <= 7 {
    return "expires this week"
  }
  if item.opened {
    return "opened pantry item should be rotated before sealed stock"
  }
  "normal pantry stock"
}

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

///|
fn matching_pantry_item(
  pantry : Array[PantryItem],
  ingredient : Ingredient,
) -> PantryItem? {
  let mut best : PantryItem? = None
  let mut best_score = -1
  for item in pantry {
    if same_name(item.name, ingredient.name) &&
      same_measure_kind(item.quantity, ingredient.quantity) {
      let score = rotation_priority(item)
      if score > best_score {
        best = Some(item)
        best_score = score
      }
    }
  }
  best
}

///|
pub fn rotation_plan(
  recipe : Recipe,
  pantry : Array[PantryItem],
) -> Array[RotationLine] {
  let lines = Array::new(capacity=recipe.ingredients.length())
  for ingredient in recipe.ingredients {
    match matching_pantry_item(pantry, ingredient) {
      Some(item) => {
        let use_first = min_quantity(ingredient.quantity, item.quantity)
        let buy = quantity_missing(ingredient.quantity, item.quantity)
        lines.push(
          RotationLine(
            name=ingredient.name,
            required=ingredient.quantity,
            available=item.quantity,
            use_first~,
            buy~,
            priority=rotation_priority(item),
            reason=rotation_reason(item),
          ),
        )
      }
      None =>
        lines.push(
          RotationLine(
            name=ingredient.name,
            required=ingredient.quantity,
            available=Quantity(amount=0, unit=ingredient.quantity.unit),
            use_first=Quantity(amount=0, unit=ingredient.quantity.unit),
            buy=ingredient.quantity,
            priority=0,
            reason="not available in pantry",
          ),
        )
    }
  }
  lines
}

///|
pub fn rotation_score(lines : Array[RotationLine]) -> Int {
  let mut score = 0
  for line in lines {
    score += line.priority
    if line.buy.amount > 0 {
      score += 10
    }
  }
  score
}

///|
pub fn urgent_rotation_count(lines : Array[RotationLine]) -> Int {
  let mut count = 0
  for line in lines {
    if line.priority >= 70 {
      count += 1
    }
  }
  count
}

///|
pub fn expiring_not_in_recipe(
  recipe : Recipe,
  pantry : Array[PantryItem],
  window_days : Int,
) -> Array[PantryWarning] {
  let warnings = Array::new()
  for item in pantry {
    if item.expires_in_days <= window_days && !recipe_contains(recipe, item) {
      warnings.push(
        PantryWarning(
          code="unused-expiring",
          message=item.name + " expires soon but is not used by this recipe.",
          severity="info",
        ),
      )
    }
  }
  warnings
}

///|
pub fn rotation_summary(lines : Array[RotationLine]) -> String {
  let out = StringBuilder()
  out.write_string("rotation-lines=" + lines.length().to_string())
  out.write_string(" urgent=" + urgent_rotation_count(lines).to_string())
  out.write_string(" score=" + rotation_score(lines).to_string())
  for line in lines {
    if line.priority >= 70 || line.buy.amount > 0 {
      out.write_string(" | ")
      out.write_string(line.name)
      out.write_string(": use ")
      out.write_string(quantity_text(line.use_first))
      out.write_string(", buy ")
      out.write_string(quantity_text(line.buy))
      out.write_string(", ")
      out.write_string(line.reason)
    }
  }
  out.to_string()
}