///|
pub struct CostedRepairPlan {
  priv plan : RepairPlan
  total_cost : Int64
}

///|
pub fn CostedRepairPlan::plan(self : CostedRepairPlan) -> RepairPlan {
  self.plan
}

///|
pub fn CostedRepairPlan::total_cost(self : CostedRepairPlan) -> Int64 {
  self.total_cost
}

///|
/// Select at most k available shards by nonnegative host costs, tie-breaking
/// by shard index. Costs are abstract units, not measured network latency.
pub fn Codec::plan_costed(
  self : Codec,
  present : Array[Bool],
  costs : Array[Int64],
) -> CostedRepairPlan raise ErasureError {
  let base = self.plan(present)
  if costs.length() != self.total_count() {
    raise InvalidShardCount(expected=self.total_count(), actual=costs.length())
  }
  let candidates : Array[Int32] = []
  for i = 0; i < costs.length(); i = i + 1 {
    if costs[i] < 0L {
      raise InvalidConfiguration("negative shard cost")
    }
    if present[i] {
      candidates.push(i)
    }
  }
  candidates.sort_by((a, b) => {
    if costs[a] < costs[b] {
      -1
    } else if costs[a] > costs[b] {
      1
    } else {
      a - b
    }
  })
  let selected : Array[Int32] = []
  let mut total = 0L
  for i = 0; i < candidates.length() && i < self.data_count(); i = i + 1 {
    let index = candidates[i]
    if costs[index] > 9223372036854775807L - total {
      raise ResourceLimit("selected cost overflow")
    }
    total += costs[index]
    selected.push(index)
  }
  {
    plan: {
      required: base.required,
      available: base.available,
      selected_indices: selected,
      missing_data_indices: base.missing_data_indices.copy(),
      missing_parity_indices: base.missing_parity_indices.copy(),
    },
    total_cost: total,
  }
}

///|
pub struct DomainExposure {
  domain : String
  lost_available : Int32
  remaining : Int32
  spare : Int32
}

///|
pub fn DomainExposure::domain(self : DomainExposure) -> String {
  self.domain
}

///|
pub fn DomainExposure::lost_available(self : DomainExposure) -> Int32 {
  self.lost_available
}

///|
pub fn DomainExposure::remaining(self : DomainExposure) -> Int32 {
  self.remaining
}

///|
/// Remaining known-intact shards minus k. Negative is a recovery deficit.
pub fn DomainExposure::spare(self : DomainExposure) -> Int32 {
  self.spare
}

///|
pub fn DomainExposure::recoverable(self : DomainExposure) -> Bool {
  self.spare >= 0
}

///|
/// Evaluate losing each named failure domain independently, given the current
/// known-intact pattern. This does not certify placement independence or CRCs.
pub fn Codec::assess_domains(
  self : Codec,
  present : Array[Bool],
  placement : Array[String],
) -> Array[DomainExposure] raise ErasureError {
  let base = self.plan(present)
  if placement.length() != self.total_count() {
    raise InvalidShardCount(
      expected=self.total_count(),
      actual=placement.length(),
    )
  }
  let counts : Map[String, Int32] = Map([])
  for i = 0; i < placement.length(); i = i + 1 {
    let domain = placement[i]
    if domain.is_empty() {
      raise InvalidConfiguration("empty failure domain")
    }
    counts[domain] = counts.get(domain).unwrap_or(0) +
      (if present[i] { 1 } else { 0 })
  }
  let names : Array[String] = []
  for name, _ in counts {
    names.push(name)
  }
  names.sort_by((a, b) => a.lexical_compare(b))
  names.map(name => {
    let lost = counts[name]
    let remaining = base.available - lost
    {
      domain: name,
      lost_available: lost,
      remaining,
      spare: remaining - self.data_count(),
    }
  })
}