///|
/// A component target used in reliability allocation studies.
pub struct AllocationComponent {
  component_id : Int
  name : String
  current_reliability : Double
  target_reliability : Double
  cost : Double
  weight : Double
  failure_rate : Double
  repair_time : Double
}

///|
pub fn allocation_component(
  component_id : Int,
  name : String,
  current_reliability : Double,
  target_reliability : Double,
  cost : Double,
  weight : Double,
  failure_rate : Double,
  repair_time : Double,
) -> AllocationComponent {
  if component_id < 0 ||
    current_reliability <= 0.0 ||
    current_reliability > 1.0 ||
    target_reliability <= 0.0 ||
    target_reliability > 1.0 ||
    cost < 0.0 ||
    weight <= 0.0 ||
    failure_rate < 0.0 ||
    repair_time < 0.0 {
    abort("invalid allocation component")
  }
  {
    component_id,
    name,
    current_reliability,
    target_reliability,
    cost,
    weight,
    failure_rate,
    repair_time,
  }
}

///|
pub fn allocation_gap(component : AllocationComponent) -> Double {
  component.target_reliability - component.current_reliability
}

///|
pub fn allocation_failure_probability(
  component : AllocationComponent,
) -> Double {
  1.0 - component.current_reliability
}

///|
pub fn allocation_target_failure_probability(
  component : AllocationComponent,
) -> Double {
  1.0 - component.target_reliability
}

///|
pub fn allocation_improvement_needed(component : AllocationComponent) -> Double {
  allocation_gap(component).max(0.0)
}

///|
pub fn allocation_cost_per_reliability(
  component : AllocationComponent,
) -> Double {
  allocation_improvement_needed(component) / component.cost.max(1.0e-12)
}

///|
pub fn allocation_risk_priority(component : AllocationComponent) -> Double {
  allocation_failure_probability(component) *
  component.weight *
  (1.0 + component.failure_rate * component.repair_time)
}

///|
pub fn allocation_criticality(component : AllocationComponent) -> Double {
  component.weight * allocation_failure_probability(component)
}

///|
pub struct ReliabilityAllocationPlan {
  components : Array[AllocationComponent]
  system_target : Double
  architecture : String
  budget : Double
  system_reliability : Double
  total_cost : Double
  total_risk : Double
  feasible : Bool
}

///|
pub fn reliability_allocation_plan(
  components : Array[AllocationComponent],
  system_target : Double,
  architecture : String,
  budget : Double,
) -> ReliabilityAllocationPlan {
  if components.is_empty() ||
    system_target <= 0.0 ||
    system_target > 1.0 ||
    budget < 0.0 {
    abort("invalid allocation plan")
  }
  let reliability = if architecture == "series" {
    components.fold(init=1.0, (product, component) => {
      product * component.target_reliability
    })
  } else if architecture == "parallel" {
    let failure = components.fold(init=1.0, (product, component) => {
      product * (1.0 - component.target_reliability)
    })
    1.0 - failure
  } else {
    abort("architecture must be series or parallel")
  }
  let total_cost = components.fold(init=0.0, (total, component) => {
    total + component.cost
  })
  let total_risk = components.fold(init=0.0, (total, component) => {
    total + allocation_risk_priority(component)
  })
  {
    components,
    system_target,
    architecture,
    budget,
    system_reliability: reliability,
    total_cost,
    total_risk,
    feasible: reliability >= system_target && total_cost <= budget,
  }
}

///|
pub fn allocation_system_reliability(
  plan : ReliabilityAllocationPlan,
) -> Double {
  plan.system_reliability
}

///|
pub fn allocation_system_margin(plan : ReliabilityAllocationPlan) -> Double {
  plan.system_reliability - plan.system_target
}

///|
pub fn allocation_budget_remaining(plan : ReliabilityAllocationPlan) -> Double {
  plan.budget - plan.total_cost
}

///|
pub fn allocation_is_feasible(plan : ReliabilityAllocationPlan) -> Bool {
  plan.feasible
}

///|
pub fn allocation_component_count(plan : ReliabilityAllocationPlan) -> Int {
  plan.components.length()
}

///|
pub fn allocation_series_target(
  system_target : Double,
  component_count : Int,
) -> Double {
  if system_target <= 0.0 || system_target > 1.0 || component_count < 1 {
    abort("invalid series allocation inputs")
  }
  @math.pow(system_target, 1.0 / component_count.to_double())
}

///|
pub fn allocation_series_targets(
  system_target : Double,
  component_count : Int,
) -> Array[Double] {
  Array::make(
    component_count,
    allocation_series_target(system_target, component_count),
  )
}

///|
pub fn allocation_parallel_target(
  system_target : Double,
  component_count : Int,
) -> Double {
  if system_target <= 0.0 || system_target > 1.0 || component_count < 1 {
    abort("invalid parallel allocation inputs")
  }
  1.0 - @math.pow(1.0 - system_target, 1.0 / component_count.to_double())
}

///|
pub fn allocation_parallel_targets(
  system_target : Double,
  component_count : Int,
) -> Array[Double] {
  Array::make(
    component_count,
    allocation_parallel_target(system_target, component_count),
  )
}

///|
pub fn allocation_series_reliability(reliabilities : Array[Double]) -> Double {
  if reliabilities.is_empty() {
    abort("series requires components")
  }
  reliabilities.fold(init=1.0, (product, value) => {
    product * value.max(0.0).min(1.0)
  })
}

///|
pub fn allocation_parallel_reliability(reliabilities : Array[Double]) -> Double {
  if reliabilities.is_empty() {
    abort("parallel requires components")
  }
  1.0 -
  reliabilities.fold(init=1.0, (product, value) => {
    product * (1.0 - value.max(0.0).min(1.0))
  })
}

///|
pub fn allocation_k_out_of_n_reliability(
  reliability : Double,
  count : Int,
  required : Int,
) -> Double {
  if reliability < 0.0 ||
    reliability > 1.0 ||
    count < 1 ||
    required < 1 ||
    required > count {
    abort("invalid k-out-of-n inputs")
  }
  let mut result = 0.0
  for successes in required..<=count {
    result += @math.exp(
      log_factorial(count) -
      log_factorial(successes) -
      log_factorial(count - successes) +
      successes.to_double() * safe_log_probability(reliability) +
      (count - successes).to_double() * safe_log_probability(1.0 - reliability),
    )
  }
  result.min(1.0).max(0.0)
}

///|
pub fn allocation_k_out_of_n_failure(
  reliability : Double,
  count : Int,
  required : Int,
) -> Double {
  1.0 - allocation_k_out_of_n_reliability(reliability, count, required)
}

///|
pub fn allocation_redundancy_gain(
  reliability : Double,
  added_units : Int,
) -> Double {
  if added_units < 0 {
    abort("added units must be non-negative")
  }
  allocation_parallel_reliability(Array::make(added_units + 1, reliability)) -
  reliability
}

///|
pub fn allocation_redundancy_count(
  target : Double,
  component_reliability : Double,
  maximum_units : Int,
) -> Int {
  if target <= 0.0 ||
    target > 1.0 ||
    component_reliability <= 0.0 ||
    component_reliability > 1.0 ||
    maximum_units < 1 {
    abort("invalid redundancy inputs")
  }
  for units in 1..<=maximum_units {
    if allocation_parallel_reliability(
        Array::make(units, component_reliability),
      ) >=
      target {
      return units
    }
  }
  maximum_units
}

///|
pub fn allocation_redundancy_cost(
  target : Double,
  component_reliability : Double,
  unit_cost : Double,
  maximum_units : Int,
) -> Double {
  if unit_cost < 0.0 {
    abort("unit cost must be non-negative")
  }
  allocation_redundancy_count(target, component_reliability, maximum_units).to_double() *
  unit_cost
}

///|
pub fn allocation_marginal_reliability(
  reliabilities : Array[Double],
  component_index : Int,
  architecture : String,
) -> Double {
  if component_index < 0 || component_index >= reliabilities.length() {
    abort("component index out of range")
  }
  let baseline = if architecture == "series" {
    allocation_series_reliability(reliabilities)
  } else {
    allocation_parallel_reliability(reliabilities)
  }
  let improved = reliabilities.copy()
  improved[component_index] = (improved[component_index] + 1.0e-5).min(1.0)
  let next = if architecture == "series" {
    allocation_series_reliability(improved)
  } else {
    allocation_parallel_reliability(improved)
  }
  (next - baseline) / 1.0e-5
}

///|
pub fn allocation_importance_factors(
  reliabilities : Array[Double],
  architecture : String,
) -> Array[Double] {
  Array::makei(reliabilities.length(), i => {
    allocation_marginal_reliability(reliabilities, i, architecture)
  })
}

///|
pub fn allocation_importance_rank(
  reliabilities : Array[Double],
  architecture : String,
) -> Array[Int] {
  let indices = Array::makei(reliabilities.length(), i => i)
  indices.sort_by((left, right) => {
    let left_value = allocation_marginal_reliability(
      reliabilities, left, architecture,
    )
    let right_value = allocation_marginal_reliability(
      reliabilities, right, architecture,
    )
    if left_value > right_value {
      -1
    } else if left_value < right_value {
      1
    } else {
      0
    }
  })
  indices
}

///|
pub fn allocation_risk_rank(
  components : Array[AllocationComponent],
) -> Array[AllocationComponent] {
  let result = components.copy()
  result.sort_by((left, right) => {
    let left_risk = allocation_risk_priority(left)
    let right_risk = allocation_risk_priority(right)
    if left_risk > right_risk {
      -1
    } else if left_risk < right_risk {
      1
    } else {
      0
    }
  })
  result
}

///|
pub fn allocation_cost_rank(
  components : Array[AllocationComponent],
) -> Array[AllocationComponent] {
  let result = components.copy()
  result.sort_by((left, right) => {
    let left_value = allocation_cost_per_reliability(left)
    let right_value = allocation_cost_per_reliability(right)
    if left_value > right_value {
      -1
    } else if left_value < right_value {
      1
    } else {
      0
    }
  })
  result
}

///|
pub fn allocation_priority_scores(
  components : Array[AllocationComponent],
) -> Array[Double] {
  components.map(component => {
    allocation_risk_priority(component) +
    allocation_improvement_needed(component)
  })
}

///|
pub fn allocation_total_improvement(
  components : Array[AllocationComponent],
) -> Double {
  components.fold(init=0.0, (total, component) => {
    total + allocation_improvement_needed(component)
  })
}

///|
pub fn allocation_total_cost(components : Array[AllocationComponent]) -> Double {
  components.fold(init=0.0, (total, component) => total + component.cost)
}

///|
pub fn allocation_total_risk(components : Array[AllocationComponent]) -> Double {
  components.fold(init=0.0, (total, component) => {
    total + allocation_risk_priority(component)
  })
}

///|
pub fn allocation_budget_select(
  components : Array[AllocationComponent],
  budget : Double,
) -> Array[AllocationComponent] {
  if budget < 0.0 {
    abort("budget must be non-negative")
  }
  let result = []
  let mut remaining = budget
  let ranked = allocation_risk_rank(components)
  for component in ranked {
    if component.cost <= remaining {
      result.push(component)
      remaining -= component.cost
    }
  }
  result
}

///|
pub fn allocation_selection_budget_remaining(
  components : Array[AllocationComponent],
  budget : Double,
) -> Double {
  budget - allocation_total_cost(allocation_budget_select(components, budget))
}

///|
pub fn allocation_budget_utilization(
  components : Array[AllocationComponent],
  budget : Double,
) -> Double {
  if budget <= 0.0 {
    abort("budget must be positive")
  }
  allocation_total_cost(allocation_budget_select(components, budget)) / budget
}

///|
pub fn allocation_system_target_met(
  reliabilities : Array[Double],
  system_target : Double,
  architecture : String,
) -> Bool {
  let actual = if architecture == "series" {
    allocation_series_reliability(reliabilities)
  } else {
    allocation_parallel_reliability(reliabilities)
  }
  actual >= system_target
}

///|
pub fn allocation_target_adjustment(
  reliabilities : Array[Double],
  system_target : Double,
  architecture : String,
) -> Array[Double] {
  if architecture == "series" {
    let target = allocation_series_target(system_target, reliabilities.length())
    reliabilities.map(value => (target - value).max(0.0))
  } else {
    let target = allocation_parallel_target(
      system_target,
      reliabilities.length(),
    )
    reliabilities.map(value => (target - value).max(0.0))
  }
}

///|
pub fn allocation_target_sensitivity(
  reliabilities : Array[Double],
  system_target : Double,
  architecture : String,
) -> Array[Double] {
  let adjustments = allocation_target_adjustment(
    reliabilities, system_target, architecture,
  )
  let total = adjustments.fold(init=0.0, (sum, value) => sum + value)
  if total == 0.0 {
    adjustments.map(_ => 0.0)
  } else {
    adjustments.map(value => value / total)
  }
}

///|
pub fn allocation_reliability_checksum(
  components : Array[AllocationComponent],
) -> Double {
  components.fold(init=0.0, (total, component) => {
    total +
    component.component_id.to_double() +
    component.current_reliability +
    component.target_reliability +
    component.cost +
    component.weight +
    component.failure_rate +
    component.repair_time
  })
}

///|
pub struct AllocationScenario {
  name : String
  reliability : Double
  cost : Double
  risk : Double
  score : Double
}

///|
pub fn allocation_scenario(
  name : String,
  reliability : Double,
  cost : Double,
  risk : Double,
  score : Double,
) -> AllocationScenario {
  { name, reliability, cost, risk, score }
}

///|
pub fn allocation_scenario_score(scenario : AllocationScenario) -> Double {
  scenario.score
}

///|
pub fn allocation_scenario_rank(
  scenarios : Array[AllocationScenario],
) -> Array[AllocationScenario] {
  let result = scenarios.copy()
  result.sort_by((left, right) => {
    if left.score > right.score {
      -1
    } else if left.score < right.score {
      1
    } else {
      0
    }
  })
  result
}

///|
pub fn allocation_best_scenario(
  scenarios : Array[AllocationScenario],
) -> AllocationScenario {
  allocation_scenario_rank(scenarios)[0]
}

///|
pub fn allocation_scenario_feasible(
  scenario : AllocationScenario,
  target : Double,
  budget : Double,
) -> Bool {
  scenario.reliability >= target && scenario.cost <= budget
}

///|
pub fn allocation_scenario_filter(
  scenarios : Array[AllocationScenario],
  target : Double,
  budget : Double,
) -> Array[AllocationScenario] {
  scenarios.filter(scenario => {
    allocation_scenario_feasible(scenario, target, budget)
  })
}

///|
pub fn allocation_scenario_checksum(
  scenarios : Array[AllocationScenario],
) -> Double {
  scenarios.fold(init=0.0, (total, scenario) => {
    total +
    scenario.reliability +
    scenario.cost +
    scenario.risk +
    scenario.score
  })
}

///|
pub fn allocation_series_failure_rate(
  rates : Array[Double],
  mission_time : Double,
) -> Double {
  if mission_time < 0.0 {
    abort("mission time must be non-negative")
  }
  1.0 -
  @math.exp(
    -rates.fold(init=0.0, (total, value) => total + value.max(0.0)) *
    mission_time,
  )
}

///|
pub fn allocation_parallel_failure_rate(
  rates : Array[Double],
  mission_time : Double,
) -> Double {
  if mission_time < 0.0 {
    abort("mission time must be non-negative")
  }
  rates.fold(init=1.0, (survival, rate) => {
    survival * (1.0 - (1.0 - @math.exp(-rate.max(0.0) * mission_time)))
  })
}

///|
pub fn allocation_series_availability(availability : Array[Double]) -> Double {
  allocation_series_reliability(availability)
}

///|
pub fn allocation_parallel_availability(availability : Array[Double]) -> Double {
  allocation_parallel_reliability(availability)
}

///|
pub fn allocation_redundancy_availability(
  component_availability : Double,
  component_count : Int,
  required : Int,
) -> Double {
  allocation_k_out_of_n_reliability(
    component_availability, component_count, required,
  )
}

///|
pub fn allocation_repairable_series_availability(
  failure_rates : Array[Double],
  repair_rates : Array[Double],
) -> Double {
  if failure_rates.length() != repair_rates.length() || failure_rates.is_empty() {
    abort("rate vectors must have same non-empty length")
  }
  allocation_series_availability(
    Array::makei(failure_rates.length(), i => {
      let failure = failure_rates[i].max(0.0)
      let repair = repair_rates[i].max(0.0)
      if failure + repair == 0.0 {
        1.0
      } else {
        repair / (failure + repair)
      }
    }),
  )
}

///|
pub fn allocation_repairable_parallel_availability(
  failure_rates : Array[Double],
  repair_rates : Array[Double],
) -> Double {
  if failure_rates.length() != repair_rates.length() || failure_rates.is_empty() {
    abort("rate vectors must have same non-empty length")
  }
  allocation_parallel_availability(
    Array::makei(failure_rates.length(), i => {
      let failure = failure_rates[i].max(0.0)
      let repair = repair_rates[i].max(0.0)
      if failure + repair == 0.0 {
        1.0
      } else {
        repair / (failure + repair)
      }
    }),
  )
}

///|
pub fn allocation_component_contribution(
  component : AllocationComponent,
  architecture : String,
) -> Double {
  if architecture == "series" {
    component.current_reliability
  } else {
    1.0 - component.current_reliability
  }
}

///|
pub fn allocation_component_target_contribution(
  component : AllocationComponent,
  architecture : String,
) -> Double {
  if architecture == "series" {
    component.target_reliability
  } else {
    1.0 - component.target_reliability
  }
}

///|
pub fn allocation_component_delta_score(
  component : AllocationComponent,
  architecture : String,
) -> Double {
  (allocation_component_target_contribution(component, architecture) -
  allocation_component_contribution(component, architecture)).abs() *
  component.weight
}

///|
pub fn allocation_delta_scores(
  components : Array[AllocationComponent],
  architecture : String,
) -> Array[Double] {
  components.map(component => {
    allocation_component_delta_score(component, architecture)
  })
}

///|
pub fn allocation_delta_rank(
  components : Array[AllocationComponent],
  architecture : String,
) -> Array[AllocationComponent] {
  let result = components.copy()
  result.sort_by((left, right) => {
    let left_score = allocation_component_delta_score(left, architecture)
    let right_score = allocation_component_delta_score(right, architecture)
    if left_score > right_score {
      -1
    } else if left_score < right_score {
      1
    } else {
      0
    }
  })
  result
}