///|
/// A repairable asset with failure and restoration rates.
pub struct RepairableAsset {
  asset_id : Int
  failure_rate : Double
  repair_rate : Double
  preventive_rate : Double
  spare_count : Int
  repair_cost : Double
  downtime_cost : Double
}

///|
pub fn repairable_asset(
  asset_id : Int,
  failure_rate : Double,
  repair_rate : Double,
  preventive_rate : Double,
  spare_count : Int,
  repair_cost : Double,
  downtime_cost : Double,
) -> RepairableAsset {
  if asset_id < 0 ||
    failure_rate < 0.0 ||
    repair_rate < 0.0 ||
    preventive_rate < 0.0 ||
    spare_count < 0 ||
    repair_cost < 0.0 ||
    downtime_cost < 0.0 {
    abort("invalid repairable asset")
  }
  {
    asset_id,
    failure_rate,
    repair_rate,
    preventive_rate,
    spare_count,
    repair_cost,
    downtime_cost,
  }
}

///|
pub fn repairable_asset_mtbf(asset : RepairableAsset) -> Double {
  if asset.failure_rate == 0.0 {
    1.0e300
  } else {
    1.0 / asset.failure_rate
  }
}

///|
pub fn repairable_asset_mttr(asset : RepairableAsset) -> Double {
  if asset.repair_rate == 0.0 {
    1.0e300
  } else {
    1.0 / asset.repair_rate
  }
}

///|
pub fn repairable_asset_inherent_availability(
  asset : RepairableAsset,
) -> Double {
  let total = asset.failure_rate + asset.repair_rate
  if total == 0.0 {
    1.0
  } else {
    asset.repair_rate / total
  }
}

///|
pub fn repairable_asset_steady_failure_probability(
  asset : RepairableAsset,
) -> Double {
  1.0 - repairable_asset_inherent_availability(asset)
}

///|
pub fn repairable_asset_effective_failure_rate(
  asset : RepairableAsset,
) -> Double {
  asset.failure_rate * (1.0 - asset.preventive_rate.min(1.0))
}

///|
pub fn repairable_asset_effective_availability(
  asset : RepairableAsset,
) -> Double {
  let failure = repairable_asset_effective_failure_rate(asset)
  let total = failure + asset.repair_rate
  if total == 0.0 {
    1.0
  } else {
    asset.repair_rate / total
  }
}

///|
pub fn repairable_asset_expected_failures(
  asset : RepairableAsset,
  horizon : Double,
) -> Double {
  if horizon < 0.0 {
    abort("horizon must be non-negative")
  }
  repairable_asset_effective_failure_rate(asset) * horizon
}

///|
pub fn repairable_asset_expected_downtime(
  asset : RepairableAsset,
  horizon : Double,
) -> Double {
  repairable_asset_expected_failures(asset, horizon) *
  repairable_asset_mttr(asset)
}

///|
pub fn repairable_asset_expected_cost(
  asset : RepairableAsset,
  horizon : Double,
) -> Double {
  repairable_asset_expected_failures(asset, horizon) * asset.repair_cost +
  repairable_asset_expected_downtime(asset, horizon) * asset.downtime_cost
}

///|
pub fn repairable_asset_reliability(
  asset : RepairableAsset,
  mission_time : Double,
) -> Double {
  if mission_time < 0.0 {
    abort("mission time must be non-negative")
  }
  @math.exp(-repairable_asset_effective_failure_rate(asset) * mission_time)
}

///|
pub struct RepairableSystem {
  assets : Array[RepairableAsset]
  architecture : String
  repair_capacity : Int
  spare_pool : Int
  name : String
}

///|
pub fn repairable_system(
  assets : Array[RepairableAsset],
  architecture : String,
  repair_capacity : Int,
  spare_pool : Int,
  name : String,
) -> RepairableSystem {
  if assets.is_empty() ||
    (architecture != "series" && architecture != "parallel") ||
    repair_capacity < 1 ||
    spare_pool < 0 {
    abort("invalid repairable system")
  }
  { assets, architecture, repair_capacity, spare_pool, name }
}

///|
pub fn repairable_system_asset_count(system : RepairableSystem) -> Int {
  system.assets.length()
}

///|
pub fn repairable_system_failure_rates(
  system : RepairableSystem,
) -> Array[Double] {
  system.assets.map(asset => asset.failure_rate)
}

///|
pub fn repairable_system_repair_rates(
  system : RepairableSystem,
) -> Array[Double] {
  system.assets.map(asset => asset.repair_rate)
}

///|
pub fn repairable_system_availability(system : RepairableSystem) -> Double {
  let values = system.assets.map(asset => {
    repairable_asset_effective_availability(asset)
  })
  if system.architecture == "series" {
    allocation_series_availability(values)
  } else {
    allocation_parallel_availability(values)
  }
}

///|
pub fn repairable_system_reliability(
  system : RepairableSystem,
  mission_time : Double,
) -> Double {
  let values = system.assets.map(asset => {
    repairable_asset_reliability(asset, mission_time)
  })
  if system.architecture == "series" {
    allocation_series_reliability(values)
  } else {
    allocation_parallel_reliability(values)
  }
}

///|
pub fn repairable_system_expected_failures(
  system : RepairableSystem,
  horizon : Double,
) -> Double {
  system.assets.fold(init=0.0, (total, asset) => {
    total + repairable_asset_expected_failures(asset, horizon)
  })
}

///|
pub fn repairable_system_expected_downtime(
  system : RepairableSystem,
  horizon : Double,
) -> Double {
  if system.architecture == "series" {
    system.assets.fold(init=0.0, (total, asset) => {
      total + repairable_asset_expected_downtime(asset, horizon)
    })
  } else {
    system.assets.fold(init=0.0, (total, asset) => {
      total + repairable_asset_expected_downtime(asset, horizon)
    }) /
    system.assets.length().to_double()
  }
}

///|
pub fn repairable_system_expected_cost(
  system : RepairableSystem,
  horizon : Double,
) -> Double {
  system.assets.fold(init=0.0, (total, asset) => {
    total + repairable_asset_expected_cost(asset, horizon)
  })
}

///|
pub fn repairable_system_spare_demand(
  system : RepairableSystem,
  horizon : Double,
) -> Double {
  repairable_system_expected_failures(system, horizon) /
  system.repair_capacity.to_double()
}

///|
pub fn repairable_system_spare_shortage(
  system : RepairableSystem,
  horizon : Double,
) -> Double {
  (repairable_system_spare_demand(system, horizon) -
  system.spare_pool.to_double()).max(0.0)
}

///|
pub fn repairable_system_spare_coverage(
  system : RepairableSystem,
  horizon : Double,
) -> Double {
  let demand = repairable_system_spare_demand(system, horizon)
  if demand == 0.0 {
    1.0
  } else {
    (system.spare_pool.to_double() / demand).min(1.0)
  }
}

///|
pub fn repairable_system_service_level(
  system : RepairableSystem,
  horizon : Double,
) -> Double {
  repairable_system_availability(system) *
  repairable_system_spare_coverage(system, horizon)
}

///|
pub struct RepairEvent {
  asset_id : Int
  failure_time : Double
  repair_start : Double
  repair_end : Double
  severity : Int
}

///|
pub fn repair_event(
  asset_id : Int,
  failure_time : Double,
  repair_start : Double,
  repair_end : Double,
  severity : Int,
) -> RepairEvent {
  if asset_id < 0 ||
    failure_time < 0.0 ||
    repair_start < failure_time ||
    repair_end < repair_start ||
    severity < 0 {
    abort("invalid repair event")
  }
  { asset_id, failure_time, repair_start, repair_end, severity }
}

///|
pub fn repair_event_wait(event : RepairEvent) -> Double {
  event.repair_start - event.failure_time
}

///|
pub fn repair_event_duration(event : RepairEvent) -> Double {
  event.repair_end - event.repair_start
}

///|
pub fn repair_event_total_outage(event : RepairEvent) -> Double {
  event.repair_end - event.failure_time
}

///|
pub fn repair_event_rate(_event : RepairEvent, horizon : Double) -> Double {
  if horizon <= 0.0 {
    abort("horizon must be positive")
  }
  1.0 / horizon
}

///|
pub fn repair_event_weight(event : RepairEvent) -> Double {
  (event.severity + 1).to_double() * repair_event_total_outage(event)
}

///|
pub struct RepairHistorySummary {
  event_count : Int
  total_wait : Double
  total_repair : Double
  total_outage : Double
  mean_wait : Double
  mean_repair : Double
  mean_outage : Double
  severity_weight : Double
  mtbf : Double
  mttr : Double
}

///|
pub fn repair_history_summary(
  events : Array[RepairEvent],
  horizon : Double,
) -> RepairHistorySummary {
  if horizon <= 0.0 {
    abort("horizon must be positive")
  }
  let wait = events.fold(init=0.0, (total, event) => {
    total + repair_event_wait(event)
  })
  let repair = events.fold(init=0.0, (total, event) => {
    total + repair_event_duration(event)
  })
  let outage = events.fold(init=0.0, (total, event) => {
    total + repair_event_total_outage(event)
  })
  let count = events.length()
  {
    event_count: count,
    total_wait: wait,
    total_repair: repair,
    total_outage: outage,
    mean_wait: if count == 0 {
      0.0
    } else {
      wait / count.to_double()
    },
    mean_repair: if count == 0 {
      0.0
    } else {
      repair / count.to_double()
    },
    mean_outage: if count == 0 {
      0.0
    } else {
      outage / count.to_double()
    },
    severity_weight: events.fold(init=0.0, (total, event) => {
      total + repair_event_weight(event)
    }),
    mtbf: if count == 0 {
      horizon
    } else {
      (horizon - outage).max(0.0) / count.to_double()
    },
    mttr: if count == 0 {
      0.0
    } else {
      repair / count.to_double()
    },
  }
}

///|
pub fn repair_history_availability(
  summary : RepairHistorySummary,
  horizon : Double,
) -> Double {
  if horizon <= 0.0 {
    abort("horizon must be positive")
  }
  (1.0 - summary.total_outage / horizon).max(0.0).min(1.0)
}

///|
pub fn repair_history_failure_rate(
  summary : RepairHistorySummary,
  horizon : Double,
) -> Double {
  if horizon <= 0.0 {
    abort("horizon must be positive")
  }
  summary.event_count.to_double() / horizon
}

///|
pub fn repair_history_weighted_availability(
  summary : RepairHistorySummary,
  horizon : Double,
) -> Double {
  let burden = summary.severity_weight / horizon
  (1.0 - burden).max(0.0).min(1.0)
}

///|
pub fn repair_history_checksum(summary : RepairHistorySummary) -> Double {
  summary.event_count.to_double() +
  summary.total_wait +
  summary.total_repair +
  summary.total_outage +
  summary.severity_weight +
  summary.mtbf +
  summary.mttr
}

///|
pub fn repair_events_by_asset(
  events : Array[RepairEvent],
  asset_id : Int,
) -> Array[RepairEvent] {
  events.filter(event => event.asset_id == asset_id)
}

///|
pub fn repair_event_asset_ids(events : Array[RepairEvent]) -> Array[Int] {
  let ids = []
  for event in events {
    if !ids.contains(event.asset_id) {
      ids.push(event.asset_id)
    }
  }
  ids.sort()
  ids
}

///|
pub fn repair_asset_failure_counts(events : Array[RepairEvent]) -> Array[Int] {
  repair_event_asset_ids(events).map(asset_id => {
    repair_events_by_asset(events, asset_id).length()
  })
}

///|
pub fn repair_asset_downtime(events : Array[RepairEvent]) -> Array[Double] {
  repair_event_asset_ids(events).map(asset_id => {
    repair_events_by_asset(events, asset_id).fold(init=0.0, (total, event) => {
      total + repair_event_total_outage(event)
    })
  })
}

///|
pub fn repair_asset_risk(events : Array[RepairEvent]) -> Array[Double] {
  repair_event_asset_ids(events).map(asset_id => {
    repair_events_by_asset(events, asset_id).fold(init=0.0, (total, event) => {
      total + repair_event_weight(event)
    })
  })
}

///|
pub fn repair_asset_rank(events : Array[RepairEvent]) -> Array[Int] {
  let ids = repair_event_asset_ids(events)
  ids.sort_by((left, right) => {
    let left_risk = repair_events_by_asset(events, left).fold(init=0.0, (
      total,
      event,
    ) => total + repair_event_weight(event))
    let right_risk = repair_events_by_asset(events, right).fold(init=0.0, (
      total,
      event,
    ) => total + repair_event_weight(event))
    if left_risk > right_risk {
      -1
    } else if left_risk < right_risk {
      1
    } else {
      0
    }
  })
  ids
}

///|
pub struct RepairQueuePlan {
  repairers : Int
  arrival_rate : Double
  service_rate : Double
  utilization : Double
  expected_queue : Double
  expected_wait : Double
  capacity : Double
  stable : Bool
}

///|
pub fn repair_queue_plan(
  repairers : Int,
  arrival_rate : Double,
  service_rate : Double,
) -> RepairQueuePlan {
  if repairers < 1 || arrival_rate < 0.0 || service_rate <= 0.0 {
    abort("invalid repair queue")
  }
  let capacity = repairers.to_double() * service_rate
  let utilization = arrival_rate / capacity
  let stable = utilization < 1.0
  let expected_queue = if stable {
    utilization * utilization / (1.0 - utilization).max(1.0e-12)
  } else {
    1.0e300
  }
  let expected_wait = if arrival_rate == 0.0 {
    0.0
  } else {
    expected_queue / arrival_rate
  }
  {
    repairers,
    arrival_rate,
    service_rate,
    utilization,
    expected_queue,
    expected_wait,
    capacity,
    stable,
  }
}

///|
pub fn repair_queue_is_stable(plan : RepairQueuePlan) -> Bool {
  plan.stable
}

///|
pub fn repair_queue_headroom(plan : RepairQueuePlan) -> Double {
  (plan.capacity - plan.arrival_rate).max(0.0)
}

///|
pub fn repair_queue_overload(plan : RepairQueuePlan) -> Double {
  (plan.arrival_rate - plan.capacity).max(0.0)
}

///|
pub fn repair_queue_plan_utilization(plan : RepairQueuePlan) -> Double {
  plan.utilization
}

///|
pub fn repair_queue_expected_queue(plan : RepairQueuePlan) -> Double {
  plan.expected_queue
}

///|
pub fn repair_queue_expected_wait(plan : RepairQueuePlan) -> Double {
  plan.expected_wait
}

///|
pub fn repair_queue_required_repairers(
  arrival_rate : Double,
  service_rate : Double,
  maximum_utilization : Double,
) -> Int {
  if arrival_rate < 0.0 ||
    service_rate <= 0.0 ||
    maximum_utilization <= 0.0 ||
    maximum_utilization >= 1.0 {
    abort("invalid repairer sizing inputs")
  }
  (arrival_rate / service_rate / maximum_utilization).ceil().to_int().max(1)
}

///|
pub fn repair_queue_plan_checksum(plan : RepairQueuePlan) -> Double {
  plan.repairers.to_double() +
  plan.arrival_rate +
  plan.service_rate +
  plan.utilization +
  plan.expected_queue +
  plan.expected_wait +
  plan.capacity
}

///|
pub fn repairable_system_queue_plan(
  system : RepairableSystem,
  horizon : Double,
  service_rate : Double,
) -> RepairQueuePlan {
  repair_queue_plan(
    system.repair_capacity,
    repairable_system_expected_failures(system, horizon) / horizon.max(1.0e-12),
    service_rate,
  )
}

///|
pub fn repairable_system_stability(
  system : RepairableSystem,
  horizon : Double,
  service_rate : Double,
) -> Bool {
  repair_queue_is_stable(
    repairable_system_queue_plan(system, horizon, service_rate),
  )
}

///|
pub fn repairable_system_target_spares(
  system : RepairableSystem,
  horizon : Double,
  target_coverage : Double,
) -> Int {
  if target_coverage <= 0.0 || target_coverage > 1.0 {
    abort("target coverage must be in (0, 1]")
  }
  (repairable_system_spare_demand(system, horizon) * target_coverage)
  .ceil()
  .to_int()
}

///|
pub fn repairable_system_add_spares(
  system : RepairableSystem,
  additional : Int,
) -> RepairableSystem {
  if additional < 0 {
    abort("additional spares must be non-negative")
  }
  { ..system, spare_pool: system.spare_pool + additional }
}

///|
pub fn repairable_system_change_repair_capacity(
  system : RepairableSystem,
  capacity : Int,
) -> RepairableSystem {
  if capacity < 1 {
    abort("repair capacity must be positive")
  }
  { ..system, repair_capacity: capacity }
}

///|
pub fn repairable_system_checksum(system : RepairableSystem) -> Double {
  system.assets.fold(
    init=system.repair_capacity.to_double() + system.spare_pool.to_double(),
    (total, asset) => {
      total +
      asset.asset_id.to_double() +
      asset.failure_rate +
      asset.repair_rate +
      asset.preventive_rate +
      asset.repair_cost +
      asset.downtime_cost
    },
  )
}

///|
pub fn repairable_asset_rank(
  assets : Array[RepairableAsset],
) -> Array[RepairableAsset] {
  let result = assets.copy()
  result.sort_by((left, right) => {
    let left_risk = left.failure_rate * left.downtime_cost
    let right_risk = right.failure_rate * right.downtime_cost
    if left_risk > right_risk {
      -1
    } else if left_risk < right_risk {
      1
    } else {
      0
    }
  })
  result
}

///|
pub fn repairable_asset_risk_scores(
  assets : Array[RepairableAsset],
) -> Array[Double] {
  assets.map(asset => {
    asset.failure_rate * asset.downtime_cost +
    asset.repair_cost * asset.failure_rate
  })
}

///|
pub fn repairable_asset_cost_curve(
  asset : RepairableAsset,
  horizons : Array[Double],
) -> Array[Double] {
  horizons.map(horizon => repairable_asset_expected_cost(asset, horizon))
}

///|
pub fn repairable_asset_availability_curve(
  asset : RepairableAsset,
  horizons : Array[Double],
) -> Array[Double] {
  horizons.map(_ => repairable_asset_effective_availability(asset))
}

///|
pub fn repairable_asset_reliability_curve(
  asset : RepairableAsset,
  horizons : Array[Double],
) -> Array[Double] {
  horizons.map(horizon => repairable_asset_reliability(asset, horizon))
}

///|
pub fn repairable_system_curve(
  system : RepairableSystem,
  horizons : Array[Double],
) -> Array[Double] {
  horizons.map(horizon => repairable_system_reliability(system, horizon))
}

///|
pub fn repairable_system_availability_curve(
  system : RepairableSystem,
  horizons : Array[Double],
) -> Array[Double] {
  horizons.map(_ => repairable_system_availability(system))
}

///|
pub fn repairable_system_cost_curve(
  system : RepairableSystem,
  horizons : Array[Double],
) -> Array[Double] {
  horizons.map(horizon => repairable_system_expected_cost(system, horizon))
}

///|
pub fn repairable_system_checksum_curve(
  system : RepairableSystem,
  horizons : Array[Double],
) -> Double {
  repairable_system_curve(system, horizons).fold(init=0.0, (total, value) => {
    total + value
  })
}