///|
/// The observation status used by survival and reliability estimators.
pub(all) enum ObservationStatus {
  Failed
  RightCensored
  LeftCensored
  IntervalCensored
} derive(Debug, Eq)

///|
/// A single lifetime record. `time` is measured in the caller's unit.
pub struct LifeObservation {
  time : Double
  status : ObservationStatus
  cause : Int
  weight : Double
}

///|
/// Create an uncensored failure observation.
pub fn failure(time : Double) -> LifeObservation {
  { time, status: Failed, cause: 0, weight: 1.0 }
}

///|
/// Create a right-censored observation, for example a unit still running at
/// the end of a test or a customer who left the service before failure.
pub fn right_censored(time : Double) -> LifeObservation {
  { time, status: RightCensored, cause: 0, weight: 1.0 }
}

///|
/// Create a left-censored observation for a unit already failed at inspection.
pub fn left_censored(time : Double) -> LifeObservation {
  { time, status: LeftCensored, cause: 0, weight: 1.0 }
}

///|
/// Create an interval-censored observation. The interval is represented by
/// the midpoint for rank-based estimators; use `interval_record` when bounds
/// must be retained for an interval likelihood.
pub fn interval_censored(lower : Double, upper : Double) -> LifeObservation {
  if lower < 0.0 || upper < lower {
    abort("invalid censoring interval")
  }
  {
    time: (lower + upper) / 2.0,
    status: IntervalCensored,
    cause: 0,
    weight: 1.0,
  }
}

///|
/// A record that retains both endpoints of an inspection interval.
pub struct IntervalRecord {
  lower : Double
  upper : Double
  status : ObservationStatus
  weight : Double
}

///|
pub fn interval_record(lower : Double, upper : Double) -> IntervalRecord {
  if lower < 0.0 || upper < lower {
    abort("invalid censoring interval")
  }
  { lower, upper, status: IntervalCensored, weight: 1.0 }
}

///|
/// Attach a failure cause and sampling weight to an observation.
pub fn LifeObservation::with_metadata(
  self : LifeObservation,
  cause~ : Int,
  weight~ : Double,
) -> LifeObservation {
  if cause < 0 || weight <= 0.0 {
    abort("cause must be non-negative and weight must be positive")
  }
  { ..self, cause, weight }
}

///|
pub fn LifeObservation::is_failure(self : LifeObservation) -> Bool {
  self.status is Failed
}

///|
pub fn LifeObservation::is_censored(self : LifeObservation) -> Bool {
  !self.is_failure()
}

///|
/// Summary statistics returned by `summarize`.
pub struct SampleSummary {
  count : Int
  failures : Int
  censored : Int
  total_weight : Double
  mean : Double
  variance : Double
  standard_deviation : Double
  minimum : Double
  maximum : Double
  median : Double
}

///|
pub fn empty_sample_summary() -> SampleSummary {
  {
    count: 0,
    failures: 0,
    censored: 0,
    total_weight: 0.0,
    mean: 0.0,
    variance: 0.0,
    standard_deviation: 0.0,
    minimum: 0.0,
    maximum: 0.0,
    median: 0.0,
  }
}

///|
/// One step of a non-parametric survival curve.
pub struct SurvivalPoint {
  time : Double
  at_risk : Int
  events : Int
  censored : Int
  survival : Double
  standard_error : Double
  cumulative_hazard : Double
}

///|
pub fn survival_point(
  time~ : Double,
  at_risk~ : Int,
  events~ : Int,
  censored~ : Int,
  survival~ : Double,
  standard_error~ : Double,
  cumulative_hazard~ : Double,
) -> SurvivalPoint {
  {
    time,
    at_risk,
    events,
    censored,
    survival,
    standard_error,
    cumulative_hazard,
  }
}

///|
/// Result of a distribution fit.
pub struct FitResult {
  distribution : String
  parameters : Array[Double]
  log_likelihood : Double
  aic : Double
  bic : Double
  iterations : Int
  converged : Bool
  standard_errors : Array[Double]
}

///|
pub fn fit_result(
  distribution~ : String,
  parameters~ : Array[Double],
  log_likelihood~ : Double,
  aic~ : Double,
  bic~ : Double,
  iterations~ : Int,
  converged~ : Bool,
  standard_errors~ : Array[Double],
) -> FitResult {
  {
    distribution,
    parameters,
    log_likelihood,
    aic,
    bic,
    iterations,
    converged,
    standard_errors,
  }
}

///|
/// A point estimate and confidence interval for a reliability metric.
pub struct MetricEstimate {
  estimate : Double
  lower : Double
  upper : Double
  confidence_level : Double
}

///|
pub fn metric_estimate(
  estimate~ : Double,
  lower~ : Double,
  upper~ : Double,
  confidence_level~ : Double,
) -> MetricEstimate {
  { estimate, lower, upper, confidence_level }
}

///|
/// A small result type used by deterministic benchmark helpers.
pub struct BenchmarkResult {
  name : String
  iterations : Int
  elapsed_micros : Int64
  checksum : Double
  operations_per_second : Double
}

///|
pub fn benchmark_result(
  name~ : String,
  iterations~ : Int,
  elapsed_micros~ : Int64,
  checksum~ : Double,
  operations_per_second~ : Double,
) -> BenchmarkResult {
  { name, iterations, elapsed_micros, checksum, operations_per_second }
}

///|
/// Validate a lifetime record and return a descriptive error string, or None.
pub fn validate_observation(record : LifeObservation) -> String? {
  if record.time < 0.0 {
    Some("time must be non-negative")
  } else if record.weight <= 0.0 {
    Some("weight must be positive")
  } else if record.cause < 0 {
    Some("cause must be non-negative")
  } else {
    None
  }
}

///|
/// Validate a data set before passing it to an estimator.
pub fn validate_dataset(records : Array[LifeObservation]) -> Array[String] {
  let errors : Array[String] = []
  records.eachi((i, record) => {
    let validation = validate_observation(record)
    match validation {
      Some(message) => errors.push("record[\{i}]: \{message}")
      None => ()
    }
  })
  errors
}

///|
/// Return a copy of records sorted by observed time, preserving equal-time
/// observations for tied-event estimators.
pub fn sort_observations(
  records : Array[LifeObservation],
) -> Array[LifeObservation] {
  let result = records.copy()
  result.sort_by((a, b) => {
    if a.time < b.time {
      -1
    } else if a.time > b.time {
      1
    } else {
      0
    }
  })
  result
}

///|
pub fn clamp_probability(p : Double) -> Double {
  if p < 0.0 {
    0.0
  } else if p > 1.0 {
    1.0
  } else {
    p
  }
}

///|
pub fn safe_log_probability(p : Double) -> Double {
  if p <= 1.0e-300 {
    @math.ln(1.0e-300)
  } else {
    @math.ln(p)
  }
}

///|
pub fn finite_or(value : Double, fallback~ : Double) -> Double {
  if value != value {
    fallback
  } else {
    value
  }
}