///|
/// A software reliability growth model configuration.
pub(all) enum SoftwareGrowthLaw {
  SoftwareExponential
  SoftwareMusaOkumoto
  SoftwareWeibull
  SoftwareGompertz
} derive(Debug, Eq)

///|
pub struct SoftwareReliabilityModel {
  law : SoftwareGrowthLaw
  initial_defects : Double
  scale : Double
  shape : Double
  detection_efficiency : Double
  name : String
}

///|
pub fn software_reliability_model(
  law : SoftwareGrowthLaw,
  initial_defects : Double,
  scale : Double,
  shape : Double,
  detection_efficiency : Double,
  name : String,
) -> SoftwareReliabilityModel {
  if initial_defects < 0.0 ||
    scale <= 0.0 ||
    shape <= 0.0 ||
    detection_efficiency <= 0.0 ||
    detection_efficiency > 1.0 {
    abort("invalid software reliability model")
  }
  { law, initial_defects, scale, shape, detection_efficiency, name }
}

///|
pub fn software_expected_failures(
  model : SoftwareReliabilityModel,
  exposure : Double,
) -> Double {
  if exposure < 0.0 {
    abort("software exposure must be non-negative")
  }
  match model.law {
    SoftwareExponential =>
      model.initial_defects * (1.0 - @math.exp(-exposure / model.scale))
    SoftwareMusaOkumoto =>
      model.scale *
      @math.ln(1.0 + exposure / model.scale) *
      model.detection_efficiency
    SoftwareWeibull =>
      model.initial_defects *
      (1.0 - @math.exp(-@math.pow(exposure / model.scale, model.shape)))
    SoftwareGompertz =>
      model.initial_defects *
      (
        1.0 -
        @math.exp(-model.scale * (@math.exp(model.shape * exposure) - 1.0))
      )
  }
}

///|
pub fn software_remaining_defects(
  model : SoftwareReliabilityModel,
  exposure : Double,
) -> Double {
  (model.initial_defects - software_expected_failures(model, exposure)).max(0.0)
}

///|
pub fn software_failure_intensity(
  model : SoftwareReliabilityModel,
  exposure : Double,
) -> Double {
  if exposure < 0.0 {
    abort("software exposure must be non-negative")
  }
  match model.law {
    SoftwareExponential =>
      model.initial_defects / model.scale * @math.exp(-exposure / model.scale)
    SoftwareMusaOkumoto =>
      model.scale / (model.scale + exposure) * model.detection_efficiency
    SoftwareWeibull =>
      if exposure == 0.0 && model.shape < 1.0 {
        1.0e300
      } else {
        model.initial_defects *
        model.shape /
        model.scale *
        @math.pow(exposure / model.scale, model.shape - 1.0) *
        @math.exp(-@math.pow(exposure / model.scale, model.shape))
      }
    SoftwareGompertz =>
      model.initial_defects *
      model.scale *
      model.shape *
      @math.exp(model.shape * exposure) *
      @math.exp(-model.scale * (@math.exp(model.shape * exposure) - 1.0))
  }
}

///|
pub fn software_failure_rate_per_hour(
  model : SoftwareReliabilityModel,
  exposure : Double,
) -> Double {
  software_failure_intensity(model, exposure) /
  software_remaining_defects(model, exposure).max(1.0e-12)
}

///|
pub fn software_mean_time_between_failures(
  model : SoftwareReliabilityModel,
  exposure : Double,
) -> Double {
  1.0 / software_failure_intensity(model, exposure).max(1.0e-300)
}

///|
pub fn software_reliability(
  model : SoftwareReliabilityModel,
  mission_exposure : Double,
  current_exposure : Double,
) -> Double {
  if mission_exposure < 0.0 || current_exposure < 0.0 {
    abort("exposure must be non-negative")
  }
  @math.exp(
    -software_failure_intensity(model, current_exposure) * mission_exposure,
  )
}

///|
pub fn software_cumulative_intensity(
  model : SoftwareReliabilityModel,
  exposure : Double,
) -> Double {
  software_expected_failures(model, exposure)
}

///|
pub fn software_growth_efficiency(
  model : SoftwareReliabilityModel,
  exposure : Double,
) -> Double {
  if model.initial_defects == 0.0 {
    1.0
  } else {
    software_expected_failures(model, exposure) / model.initial_defects
  }
}

///|
pub fn software_defect_density(
  model : SoftwareReliabilityModel,
  code_size : Double,
  exposure : Double,
) -> Double {
  if code_size <= 0.0 {
    abort("code size must be positive")
  }
  software_remaining_defects(model, exposure) / code_size
}

///|
pub fn software_defects_removed(
  model : SoftwareReliabilityModel,
  exposure : Double,
) -> Double {
  software_expected_failures(model, exposure)
}

///|
pub struct SoftwareFailureRecord {
  time : Double
  severity : Int
  detection_cost : Double
  corrected : Bool
  component : Int
}

///|
pub fn software_failure_record(
  time : Double,
  severity : Int,
  detection_cost : Double,
  corrected : Bool,
  component : Int,
) -> SoftwareFailureRecord {
  if time < 0.0 || severity < 0 || detection_cost < 0.0 || component < 0 {
    abort("invalid software failure record")
  }
  { time, severity, detection_cost, corrected, component }
}

///|
pub fn software_record_count(records : Array[SoftwareFailureRecord]) -> Int {
  records.length()
}

///|
pub fn software_record_total_cost(
  records : Array[SoftwareFailureRecord],
) -> Double {
  records.fold(init=0.0, (total, record) => total + record.detection_cost)
}

///|
pub fn software_record_mean_time(
  records : Array[SoftwareFailureRecord],
) -> Double {
  if records.is_empty() {
    0.0
  } else {
    mean(records.map(record => record.time))
  }
}

///|
pub fn software_record_median_time(
  records : Array[SoftwareFailureRecord],
) -> Double {
  if records.is_empty() {
    0.0
  } else {
    quantile(records.map(record => record.time), 0.5)
  }
}

///|
pub fn software_record_total_severity(
  records : Array[SoftwareFailureRecord],
) -> Int {
  records.fold(init=0, (total, record) => total + record.severity)
}

///|
pub fn software_record_corrected_count(
  records : Array[SoftwareFailureRecord],
) -> Int {
  records.fold(init=0, (total, record) => {
    if record.corrected {
      total + 1
    } else {
      total
    }
  })
}

///|
pub fn software_record_correction_fraction(
  records : Array[SoftwareFailureRecord],
) -> Double {
  if records.is_empty() {
    0.0
  } else {
    software_record_corrected_count(records).to_double() /
    records.length().to_double()
  }
}

///|
pub fn software_record_failure_rate(
  records : Array[SoftwareFailureRecord],
  exposure : Double,
) -> Double {
  if exposure <= 0.0 {
    abort("exposure must be positive")
  }
  records.length().to_double() / exposure
}

///|
pub fn software_record_severity_rate(
  records : Array[SoftwareFailureRecord],
  exposure : Double,
) -> Double {
  if exposure <= 0.0 {
    abort("exposure must be positive")
  }
  software_record_total_severity(records).to_double() / exposure
}

///|
pub fn software_records_by_component(
  records : Array[SoftwareFailureRecord],
  component : Int,
) -> Array[SoftwareFailureRecord] {
  records.filter(record => record.component == component)
}

///|
pub fn software_component_ids(
  records : Array[SoftwareFailureRecord],
) -> Array[Int] {
  let result = []
  for record in records {
    if !result.contains(record.component) {
      result.push(record.component)
    }
  }
  result.sort()
  result
}

///|
pub fn software_component_counts(
  records : Array[SoftwareFailureRecord],
) -> Array[Int] {
  software_component_ids(records).map(component => {
    software_records_by_component(records, component).length()
  })
}

///|
pub fn software_component_severity(
  records : Array[SoftwareFailureRecord],
) -> Array[Int] {
  software_component_ids(records).map(component => {
    software_records_by_component(records, component).fold(init=0, (
      total,
      record,
    ) => total + record.severity)
  })
}

///|
pub fn software_component_risk(
  records : Array[SoftwareFailureRecord],
) -> Array[Double] {
  let ids = software_component_ids(records)
  ids.map(component => {
    let group = software_records_by_component(records, component)
    group.fold(init=0.0, (total, record) => {
      total + record.severity.to_double() * (1.0 + record.detection_cost)
    })
  })
}

///|
pub fn software_component_rank(
  records : Array[SoftwareFailureRecord],
) -> Array[Int] {
  let ids = software_component_ids(records)
  ids.sort_by((left, right) => {
    let left_risk = software_records_by_component(records, left).fold(
      init=0.0,
      (total, record) => {
        total + record.severity.to_double() * (1.0 + record.detection_cost)
      },
    )
    let right_risk = software_records_by_component(records, right).fold(
      init=0.0,
      (total, record) => {
        total + record.severity.to_double() * (1.0 + record.detection_cost)
      },
    )
    if left_risk > right_risk {
      -1
    } else if left_risk < right_risk {
      1
    } else {
      0
    }
  })
  ids
}

///|
pub struct SoftwareReleaseGate {
  target_intensity : Double
  target_reliability : Double
  mission_exposure : Double
  maximum_severity : Int
  maximum_open_defects : Int
  minimum_correction_fraction : Double
}

///|
pub fn software_release_gate(
  target_intensity : Double,
  target_reliability : Double,
  mission_exposure : Double,
  maximum_severity : Int,
  maximum_open_defects : Int,
  minimum_correction_fraction : Double,
) -> SoftwareReleaseGate {
  if target_intensity < 0.0 ||
    target_reliability <= 0.0 ||
    target_reliability > 1.0 ||
    mission_exposure <= 0.0 ||
    maximum_severity < 0 ||
    maximum_open_defects < 0 ||
    minimum_correction_fraction < 0.0 ||
    minimum_correction_fraction > 1.0 {
    abort("invalid release gate")
  }
  {
    target_intensity,
    target_reliability,
    mission_exposure,
    maximum_severity,
    maximum_open_defects,
    minimum_correction_fraction,
  }
}

///|
pub fn software_release_gate_intensity_passes(
  gate : SoftwareReleaseGate,
  model : SoftwareReliabilityModel,
  current_exposure : Double,
) -> Bool {
  software_failure_intensity(model, current_exposure) <= gate.target_intensity
}

///|
pub fn software_release_gate_reliability_passes(
  gate : SoftwareReleaseGate,
  model : SoftwareReliabilityModel,
  current_exposure : Double,
) -> Bool {
  software_reliability(model, gate.mission_exposure, current_exposure) >=
  gate.target_reliability
}

///|
pub fn software_release_gate_records_pass(
  gate : SoftwareReleaseGate,
  records : Array[SoftwareFailureRecord],
) -> Bool {
  software_record_total_severity(records) <= gate.maximum_severity &&
  records.length() <= gate.maximum_open_defects &&
  software_record_correction_fraction(records) >=
  gate.minimum_correction_fraction
}

///|
pub fn software_release_decision(
  gate : SoftwareReleaseGate,
  model : SoftwareReliabilityModel,
  current_exposure : Double,
  records : Array[SoftwareFailureRecord],
) -> String {
  if !software_release_gate_intensity_passes(gate, model, current_exposure) {
    "reject-intensity"
  } else if !software_release_gate_reliability_passes(
      gate, model, current_exposure,
    ) {
    "reject-reliability"
  } else if !software_release_gate_records_pass(gate, records) {
    "reject-quality"
  } else {
    "release"
  }
}

///|
pub fn software_release_score(
  gate : SoftwareReleaseGate,
  model : SoftwareReliabilityModel,
  current_exposure : Double,
  records : Array[SoftwareFailureRecord],
) -> Double {
  let intensity_score = (gate.target_intensity /
  software_failure_intensity(model, current_exposure).max(1.0e-300)).min(1.0)
  let reliability_score = (software_reliability(
    model,
    gate.mission_exposure,
    current_exposure,
  ) /
  gate.target_reliability).min(1.0)
  let quality_score = if software_record_total_severity(records) == 0 {
    1.0
  } else {
    (gate.maximum_severity.to_double() /
    software_record_total_severity(records).to_double()).min(1.0)
  }
  (0.4 * intensity_score + 0.4 * reliability_score + 0.2 * quality_score).min(
    1.0,
  )
}

///|
pub fn software_release_label(score : Double) -> String {
  if score >= 0.9 {
    "ready"
  } else if score >= 0.7 {
    "review"
  } else {
    "not-ready"
  }
}

///|
pub struct SoftwareTestSession {
  session_id : Int
  start : Double
  end : Double
  executed_cases : Int
  failed_cases : Int
  failures : Array[SoftwareFailureRecord]
}

///|
pub fn software_test_session(
  session_id : Int,
  start : Double,
  end : Double,
  executed_cases : Int,
  failed_cases : Int,
  failures : Array[SoftwareFailureRecord],
) -> SoftwareTestSession {
  if session_id < 0 ||
    end <= start ||
    executed_cases < 0 ||
    failed_cases < 0 ||
    failed_cases > executed_cases {
    abort("invalid software test session")
  }
  { session_id, start, end, executed_cases, failed_cases, failures }
}

///|
pub fn software_session_duration(session : SoftwareTestSession) -> Double {
  session.end - session.start
}

///|
pub fn software_session_case_pass_rate(session : SoftwareTestSession) -> Double {
  if session.executed_cases == 0 {
    0.0
  } else {
    (session.executed_cases - session.failed_cases).to_double() /
    session.executed_cases.to_double()
  }
}

///|
pub fn software_session_failure_rate(session : SoftwareTestSession) -> Double {
  if session.executed_cases == 0 {
    0.0
  } else {
    session.failed_cases.to_double() / session.executed_cases.to_double()
  }
}

///|
pub fn software_session_failure_density(
  session : SoftwareTestSession,
) -> Double {
  if session.executed_cases == 0 {
    0.0
  } else {
    session.failures.length().to_double() / session.executed_cases.to_double()
  }
}

///|
pub fn software_session_severity(session : SoftwareTestSession) -> Int {
  software_record_total_severity(session.failures)
}

///|
pub fn software_session_cost(session : SoftwareTestSession) -> Double {
  software_record_total_cost(session.failures)
}

///|
pub fn software_session_is_stable(
  session : SoftwareTestSession,
  maximum_failure_rate : Double,
) -> Bool {
  software_session_failure_rate(session) <= maximum_failure_rate
}

///|
pub fn software_session_checksum(session : SoftwareTestSession) -> Double {
  session.session_id.to_double() +
  session.start +
  session.end +
  session.executed_cases.to_double() +
  session.failed_cases.to_double() +
  software_session_severity(session).to_double()
}

///|
pub fn software_sessions_total_cases(
  sessions : Array[SoftwareTestSession],
) -> Int {
  sessions.fold(init=0, (total, session) => total + session.executed_cases)
}

///|
pub fn software_sessions_total_failures(
  sessions : Array[SoftwareTestSession],
) -> Int {
  sessions.fold(init=0, (total, session) => total + session.failed_cases)
}

///|
pub fn software_sessions_pass_rate(
  sessions : Array[SoftwareTestSession],
) -> Double {
  let cases = software_sessions_total_cases(sessions)
  if cases == 0 {
    0.0
  } else {
    (cases - software_sessions_total_failures(sessions)).to_double() /
    cases.to_double()
  }
}

///|
pub fn software_sessions_failure_rate(
  sessions : Array[SoftwareTestSession],
) -> Double {
  let cases = software_sessions_total_cases(sessions)
  if cases == 0 {
    0.0
  } else {
    software_sessions_total_failures(sessions).to_double() / cases.to_double()
  }
}

///|
pub fn software_sessions_duration(
  sessions : Array[SoftwareTestSession],
) -> Double {
  sessions.fold(init=0.0, (total, session) => {
    total + software_session_duration(session)
  })
}

///|
pub fn software_sessions_checksum(
  sessions : Array[SoftwareTestSession],
) -> Double {
  sessions.fold(init=0.0, (total, session) => {
    total + software_session_checksum(session)
  })
}

///|
pub fn software_model_fit(
  law : SoftwareGrowthLaw,
  records : Array[SoftwareFailureRecord],
  initial_defects : Double,
  scale : Double,
) -> SoftwareReliabilityModel {
  if records.is_empty() {
    abort("software model fit requires records")
  }
  let exposure = max_value(records.map(record => record.time)).max(1.0)
  let observed = records.length().to_double()
  let shape = if law is SoftwareWeibull {
    (observed / initial_defects.max(1.0)).max(0.1)
  } else {
    1.0
  }
  software_reliability_model(
    law,
    initial_defects.max(observed),
    scale.max(exposure),
    shape,
    1.0,
    "fitted",
  )
}

///|
pub fn software_model_rmse(
  model : SoftwareReliabilityModel,
  records : Array[SoftwareFailureRecord],
) -> Double {
  if records.is_empty() {
    0.0
  } else {
    let mut sum = 0.0
    for record in records {
      let prediction = software_expected_failures(model, record.time)
      sum += (prediction - 1.0) * (prediction - 1.0)
    }
    (sum / records.length().to_double()).sqrt()
  }
}

///|
pub fn software_model_aic(
  model : SoftwareReliabilityModel,
  records : Array[SoftwareFailureRecord],
) -> Double {
  2.0 * 5.0 +
  records.length().to_double() *
  @math.ln(software_model_rmse(model, records).max(1.0e-300))
}

///|
pub fn software_model_bic(
  model : SoftwareReliabilityModel,
  records : Array[SoftwareFailureRecord],
) -> Double {
  5.0 * @math.ln(records.length().to_double().max(1.0)) +
  records.length().to_double() *
  @math.ln(software_model_rmse(model, records).max(1.0e-300))
}

///|
pub fn software_model_compare(
  models : Array[SoftwareReliabilityModel],
  records : Array[SoftwareFailureRecord],
) -> Array[Double] {
  models.map(model => software_model_rmse(model, records))
}

///|
pub fn software_model_best(
  models : Array[SoftwareReliabilityModel],
  records : Array[SoftwareFailureRecord],
) -> SoftwareReliabilityModel {
  if models.is_empty() {
    abort("models must not be empty")
  }
  let mut best = models[0]
  for model in models[1:] {
    if software_model_rmse(model, records) < software_model_rmse(best, records) {
      best = model
    }
  }
  best
}

///|
pub fn software_model_checksum(model : SoftwareReliabilityModel) -> Double {
  model.initial_defects + model.scale + model.shape + model.detection_efficiency
}