///|
/// Customer outage record used for utility-style reliability metrics.
pub struct OutageRecord {
  customer : Int
  start : Double
  duration : Double
  customers_affected : Int
  cause : Int
}

///|
pub fn outage_record(
  customer~ : Int,
  start~ : Double,
  duration~ : Double,
  customers_affected~ : Int,
  cause~ : Int,
) -> OutageRecord {
  if customer < 0 || start < 0.0 || duration < 0.0 || customers_affected <= 0 {
    abort("invalid outage record")
  }
  { customer, start, duration, customers_affected, cause }
}

///|
pub struct OutageMetrics {
  saidi : Double
  saifi : Double
  caidi : Double
  asai : Double
  maifi : Double
  total_customer_interruptions : Int
}

///|
pub fn outage_metrics(
  saidi~ : Double,
  saifi~ : Double,
  caidi~ : Double,
  asai~ : Double,
  maifi~ : Double,
  total_customer_interruptions~ : Int,
) -> OutageMetrics {
  { saidi, saifi, caidi, asai, maifi, total_customer_interruptions }
}

///|
pub fn calculate_outage_metrics(
  outages : Array[OutageRecord],
  customers : Int,
  observation_window : Double,
) -> OutageMetrics {
  if customers <= 0 || observation_window <= 0.0 {
    abort("invalid outage population")
  }
  let total_minutes = outages.fold(init=0.0, (sum, outage) => {
    sum + outage.duration * outage.customers_affected.to_double()
  })
  let interruptions = outages.fold(init=0, (sum, outage) => {
    sum + outage.customers_affected
  })
  let saidi = total_minutes / customers.to_double()
  let saifi = interruptions.to_double() / customers.to_double()
  let caidi = if interruptions == 0 {
    0.0
  } else {
    total_minutes / interruptions.to_double()
  }
  let asai = 1.0 - saidi / (observation_window * 60.0)
  let momentary = outages.filter(outage => outage.duration < 5.0).length()
  let maifi = momentary.to_double() / customers.to_double()
  outage_metrics(
    saidi~,
    saifi~,
    caidi~,
    asai~,
    maifi~,
    total_customer_interruptions=interruptions,
  )
}

///|
pub fn outage_cause_counts(outages : Array[OutageRecord]) -> Map[Int, Int] {
  let result : Map[Int, Int] = Map([])
  for outage in outages {
    result.update_or_default(outage.cause, 1, old => old + 1)
  }
  result
}

///|
pub fn restoration_curve(
  outages : Array[OutageRecord],
  grid : Array[Double],
) -> Array[Double] {
  grid.map(time => {
    let remaining = outages.fold(init=0, (sum, outage) => {
      if outage.start <= time && outage.start + outage.duration > time {
        sum + outage.customers_affected
      } else {
        sum
      }
    })
    remaining.to_double()
  })
}

///|
pub fn outage_quantiles(
  outages : Array[OutageRecord],
  probabilities : Array[Double],
) -> Array[Double] {
  let durations = outages.map(outage => outage.duration)
  probabilities.map(p => quantile(durations, p))
}

///|
pub fn longest_outage(outages : Array[OutageRecord]) -> OutageRecord {
  if outages.is_empty() {
    abort("longest outage requires records")
  }
  let mut best = outages[0]
  for outage in outages {
    if outage.duration > best.duration {
      best = outage
    }
  }
  best
}

///|
pub fn outage_rate(outages : Array[OutageRecord], window : Double) -> Double {
  if window <= 0.0 {
    abort("window must be positive")
  }
  outages.length().to_double() / window
}