///|
/// Warranty coverage policy for a product family.
pub struct ReliabilityWarranty {
  warranty_id : String
  product_code : String
  start_age : Double
  end_age : Double
  deductible : Double
  labor_rate : Double
  parts_markup : Double
  transfer_allowed : Bool
}

///|
pub fn reliability_warranty(
  warranty_id : String,
  product_code : String,
  start_age : Double,
  end_age : Double,
  deductible : Double,
  labor_rate : Double,
  parts_markup : Double,
  transfer_allowed : Bool,
) -> ReliabilityWarranty {
  if start_age < 0.0 ||
    end_age <= start_age ||
    deductible < 0.0 ||
    labor_rate < 0.0 ||
    parts_markup < 0.0 {
    abort("invalid warranty policy")
  }
  {
    warranty_id,
    product_code,
    start_age,
    end_age,
    deductible,
    labor_rate,
    parts_markup,
    transfer_allowed,
  }
}

///|
pub fn reliability_warranty_is_active(
  warranty : ReliabilityWarranty,
  age : Double,
) -> Bool {
  age >= warranty.start_age && age <= warranty.end_age
}

///|
pub fn reliability_warranty_remaining(
  warranty : ReliabilityWarranty,
  age : Double,
) -> Double {
  (warranty.end_age - age).max(0.0)
}

///|
pub fn reliability_warranty_coverage_fraction(
  warranty : ReliabilityWarranty,
  age : Double,
) -> Double {
  if age <= warranty.start_age {
    1.0
  } else if age >= warranty.end_age {
    0.0
  } else {
    (warranty.end_age - age) / (warranty.end_age - warranty.start_age)
  }
}

///|
/// A warranty claim with failure cause and service outcome.
pub struct ReliabilityWarrantyClaim {
  claim_id : Int
  asset_id : Int
  warranty_id : String
  age : Double
  opened_at : Double
  closed_at : Double
  part_cost : Double
  labor_hours : Double
  repeat_claim : Bool
  accepted : Bool
  severity : Double
}

///|
pub fn reliability_warranty_claim(
  claim_id : Int,
  asset_id : Int,
  warranty_id : String,
  age : Double,
  opened_at : Double,
  closed_at : Double,
  part_cost : Double,
  labor_hours : Double,
  repeat_claim : Bool,
  accepted : Bool,
  severity : Double,
) -> ReliabilityWarrantyClaim {
  if claim_id < 0 ||
    asset_id < 0 ||
    age < 0.0 ||
    closed_at < opened_at ||
    part_cost < 0.0 ||
    labor_hours < 0.0 ||
    severity < 0.0 {
    abort("invalid warranty claim")
  }
  {
    claim_id,
    asset_id,
    warranty_id,
    age,
    opened_at,
    closed_at,
    part_cost,
    labor_hours,
    repeat_claim,
    accepted,
    severity,
  }
}

///|
pub fn reliability_warranty_claim_cycle_time(
  claim : ReliabilityWarrantyClaim,
) -> Double {
  claim.closed_at - claim.opened_at
}

///|
pub fn reliability_warranty_claim_cost(
  warranty : ReliabilityWarranty,
  claim : ReliabilityWarrantyClaim,
) -> Double {
  if !claim.accepted {
    0.0
  } else {
    warranty.deductible +
    claim.part_cost * (1.0 + warranty.parts_markup) +
    claim.labor_hours * warranty.labor_rate
  }
}

///|
pub fn reliability_warranty_claim_is_repeat(
  claim : ReliabilityWarrantyClaim,
) -> Bool {
  claim.repeat_claim
}

///|
pub fn reliability_warranty_claim_is_late(
  claim : ReliabilityWarrantyClaim,
  service_limit : Double,
) -> Bool {
  service_limit >= 0.0 &&
  reliability_warranty_claim_cycle_time(claim) > service_limit
}

///|
/// Cohort-level warranty experience and actuarial reserve.
pub struct ReliabilityWarrantyCohort {
  cohort_id : Int
  product_code : String
  population : Int
  exposure : Double
  claims : Array[ReliabilityWarrantyClaim]
  average_price : Double
  reserve_rate : Double
}

///|
pub fn reliability_warranty_cohort(
  cohort_id : Int,
  product_code : String,
  population : Int,
  exposure : Double,
  claims : Array[ReliabilityWarrantyClaim],
  average_price : Double,
  reserve_rate : Double,
) -> ReliabilityWarrantyCohort {
  if cohort_id < 0 ||
    population < 0 ||
    exposure < 0.0 ||
    average_price < 0.0 ||
    reserve_rate < 0.0 {
    abort("invalid warranty cohort")
  }
  {
    cohort_id,
    product_code,
    population,
    exposure,
    claims,
    average_price,
    reserve_rate,
  }
}

///|
pub fn reliability_warranty_cohort_claim_count(
  cohort : ReliabilityWarrantyCohort,
) -> Int {
  cohort.claims.length()
}

///|
pub fn reliability_warranty_cohort_accepted_claims(
  cohort : ReliabilityWarrantyCohort,
) -> Int {
  cohort.claims.fold(init=0, (count, claim) => {
    if claim.accepted {
      count + 1
    } else {
      count
    }
  })
}

///|
pub fn reliability_warranty_cohort_repeat_claims(
  cohort : ReliabilityWarrantyCohort,
) -> Int {
  cohort.claims.fold(init=0, (count, claim) => {
    if claim.repeat_claim {
      count + 1
    } else {
      count
    }
  })
}

///|
pub fn reliability_warranty_cohort_claim_rate(
  cohort : ReliabilityWarrantyCohort,
) -> Double {
  if cohort.population == 0 {
    0.0
  } else {
    reliability_warranty_cohort_accepted_claims(cohort).to_double() /
    cohort.population.to_double()
  }
}

///|
pub fn reliability_warranty_cohort_repeat_rate(
  cohort : ReliabilityWarrantyCohort,
) -> Double {
  let accepted = reliability_warranty_cohort_accepted_claims(cohort)
  if accepted == 0 {
    0.0
  } else {
    reliability_warranty_cohort_repeat_claims(cohort).to_double() /
    accepted.to_double()
  }
}

///|
pub fn reliability_warranty_cohort_cost(
  cohort : ReliabilityWarrantyCohort,
  warranty : ReliabilityWarranty,
) -> Double {
  cohort.claims.fold(init=0.0, (sum, claim) => {
    sum + reliability_warranty_claim_cost(warranty, claim)
  })
}

///|
pub fn reliability_warranty_cohort_cost_per_unit(
  cohort : ReliabilityWarrantyCohort,
  warranty : ReliabilityWarranty,
) -> Double {
  if cohort.population == 0 {
    0.0
  } else {
    reliability_warranty_cohort_cost(cohort, warranty) /
    cohort.population.to_double()
  }
}

///|
pub fn reliability_warranty_cohort_expected_reserve(
  cohort : ReliabilityWarrantyCohort,
  warranty : ReliabilityWarranty,
) -> Double {
  let incurred = reliability_warranty_cohort_cost(cohort, warranty)
  let earned = cohort.population.to_double() *
    cohort.average_price *
    cohort.reserve_rate
  (earned - incurred).max(0.0)
}

///|
pub fn reliability_warranty_cohort_loss_ratio(
  cohort : ReliabilityWarrantyCohort,
  warranty : ReliabilityWarranty,
) -> Double {
  let earned = cohort.population.to_double() * cohort.average_price
  if earned == 0.0 {
    0.0
  } else {
    reliability_warranty_cohort_cost(cohort, warranty) / earned
  }
}

///|
pub fn reliability_warranty_cohort_average_cycle_time(
  cohort : ReliabilityWarrantyCohort,
) -> Double {
  if cohort.claims.is_empty() {
    0.0
  } else {
    cohort.claims.fold(init=0.0, (sum, claim) => {
      sum + reliability_warranty_claim_cycle_time(claim)
    }) /
    cohort.claims.length().to_double()
  }
}

///|
pub fn reliability_warranty_cohort_severity(
  cohort : ReliabilityWarrantyCohort,
) -> Double {
  if cohort.claims.is_empty() {
    0.0
  } else {
    cohort.claims.fold(init=0.0, (sum, claim) => sum + claim.severity) /
    cohort.claims.length().to_double()
  }
}

///|
/// Age-bucketed claim experience for early-warning monitoring.
pub struct ReliabilityWarrantyAgeBand {
  lower_age : Double
  upper_age : Double
  claim_count : Int
  accepted_count : Int
  cost : Double
  exposure : Double
}

///|
pub fn reliability_warranty_age_band(
  lower_age : Double,
  upper_age : Double,
  claim_count : Int,
  accepted_count : Int,
  cost : Double,
  exposure : Double,
) -> ReliabilityWarrantyAgeBand {
  if lower_age < 0.0 ||
    upper_age <= lower_age ||
    claim_count < 0 ||
    accepted_count < 0 ||
    cost < 0.0 ||
    exposure < 0.0 {
    abort("invalid warranty age band")
  }
  { lower_age, upper_age, claim_count, accepted_count, cost, exposure }
}

///|
pub fn reliability_warranty_age_band_rate(
  band : ReliabilityWarrantyAgeBand,
) -> Double {
  if band.exposure == 0.0 {
    0.0
  } else {
    band.accepted_count.to_double() / band.exposure
  }
}

///|
pub fn reliability_warranty_age_band_cost_rate(
  band : ReliabilityWarrantyAgeBand,
) -> Double {
  if band.exposure == 0.0 {
    0.0
  } else {
    band.cost / band.exposure
  }
}

///|
pub fn reliability_warranty_age_band_midpoint(
  band : ReliabilityWarrantyAgeBand,
) -> Double {
  (band.lower_age + band.upper_age) / 2.0
}

///|
pub fn reliability_warranty_build_age_bands(
  warranty : ReliabilityWarranty,
  claims : Array[ReliabilityWarrantyClaim],
  width : Double,
) -> Array[ReliabilityWarrantyAgeBand] {
  if width <= 0.0 {
    abort("age band width must be positive")
  }
  let result = Array::new()
  let mut lower = warranty.start_age
  while lower < warranty.end_age {
    let upper = (lower + width).min(warranty.end_age)
    let selected = claims.filter(claim => {
      claim.age >= lower && claim.age < upper
    })
    let cost = selected.fold(init=0.0, (sum, claim) => {
      sum + claim.part_cost + claim.labor_hours * warranty.labor_rate
    })
    let accepted = selected.fold(init=0, (count, claim) => {
      if claim.accepted {
        count + 1
      } else {
        count
      }
    })
    result.push(
      reliability_warranty_age_band(
        lower,
        upper,
        selected.length(),
        accepted,
        cost,
        selected.length().to_double(),
      ),
    )
    lower = upper
  }
  result
}

///|
pub fn reliability_warranty_peak_age_band(
  bands : Array[ReliabilityWarrantyAgeBand],
) -> Int {
  if bands.is_empty() {
    return -1
  }
  let mut best = 0
  for i in 1..
      reliability_warranty_age_band_cost_rate(bands[best]) {
      best = i
    }
  }
  best
}

///|
/// Forecast of future warranty liability from an empirical claim curve.
pub struct ReliabilityWarrantyForecast {
  horizon : Double
  expected_claims : Double
  expected_cost : Double
  lower_cost : Double
  upper_cost : Double
  confidence : Double
}

///|
pub fn reliability_warranty_forecast(
  horizon : Double,
  expected_claims : Double,
  expected_cost : Double,
  lower_cost : Double,
  upper_cost : Double,
  confidence : Double,
) -> ReliabilityWarrantyForecast {
  if horizon < 0.0 ||
    expected_claims < 0.0 ||
    expected_cost < 0.0 ||
    lower_cost < 0.0 ||
    upper_cost < lower_cost ||
    confidence <= 0.0 ||
    confidence >= 1.0 {
    abort("invalid warranty forecast")
  }
  {
    horizon,
    expected_claims,
    expected_cost,
    lower_cost,
    upper_cost,
    confidence,
  }
}

///|
pub fn reliability_warranty_forecast_margin(
  forecast : ReliabilityWarrantyForecast,
) -> Double {
  (forecast.upper_cost - forecast.lower_cost) / 2.0
}

///|
pub fn reliability_warranty_forecast_reserve(
  forecast : ReliabilityWarrantyForecast,
  safety_loading : Double,
) -> Double {
  if safety_loading < 0.0 {
    abort("safety loading must be non-negative")
  }
  forecast.upper_cost * (1.0 + safety_loading)
}

///|
/// Warranty model based on an age-dependent hazard curve.
pub struct ReliabilityWarrantyHazardCurve {
  ages : Array[Double]
  hazards : Array[Double]
  cumulative_claims : Array[Double]
}

///|
pub fn reliability_warranty_hazard_curve(
  ages : Array[Double],
  hazards : Array[Double],
) -> ReliabilityWarrantyHazardCurve {
  if ages.length() != hazards.length() || ages.is_empty() {
    abort("hazard curve arrays must be equal and non-empty")
  }
  let mut cumulative = 0.0
  let totals = Array::makei(ages.length(), i => {
    if hazards[i] < 0.0 {
      abort("hazard must be non-negative")
    }
    if i > 0 {
      cumulative += hazards[i] * (ages[i] - ages[i - 1]).max(0.0)
    }
    cumulative
  })
  { ages, hazards, cumulative_claims: totals }
}

///|
pub fn reliability_warranty_hazard_at(
  curve : ReliabilityWarrantyHazardCurve,
  age : Double,
) -> Double {
  if age <= curve.ages[0] {
    curve.hazards[0]
  } else {
    let mut selected = curve.hazards[curve.hazards.length() - 1]
    for i in 0.. Double {
  if age <= curve.ages[0] {
    0.0
  } else {
    let mut total = curve.cumulative_claims[curve.cumulative_claims.length() - 1]
    for i in 1.. Double {
  1.0 - @math.exp(-reliability_warranty_cumulative_hazard_at(curve, age))
}

///|
pub fn reliability_warranty_survival_probability(
  curve : ReliabilityWarrantyHazardCurve,
  age : Double,
) -> Double {
  @math.exp(-reliability_warranty_cumulative_hazard_at(curve, age))
}

///|
pub fn reliability_warranty_expected_claim_cost(
  curve : ReliabilityWarrantyHazardCurve,
  warranty : ReliabilityWarranty,
  average_claim_cost : Double,
  population : Int,
) -> Double {
  if average_claim_cost < 0.0 || population < 0 {
    abort("invalid expected claim cost")
  }
  population.to_double() *
  reliability_warranty_claim_probability(curve, warranty.end_age) *
  average_claim_cost
}

///|
pub fn reliability_warranty_claim_cost_quantiles(
  curve : ReliabilityWarrantyHazardCurve,
  average_claim_cost : Double,
  population : Int,
  probabilities : Array[Double],
) -> Array[Double] {
  if average_claim_cost < 0.0 || population < 0 {
    abort("invalid claim cost quantile input")
  }
  probabilities.map(probability => {
    if probability <= 0.0 {
      0.0
    } else if probability >= 1.0 {
      population.to_double() * average_claim_cost
    } else {
      let claim_rate = reliability_warranty_claim_probability(
        curve,
        probability * curve.ages[curve.ages.length() - 1],
      )
      population.to_double() * average_claim_cost * claim_rate
    }
  })
}

///|
pub fn reliability_warranty_claim_checksum(
  cohort : ReliabilityWarrantyCohort,
  warranty : ReliabilityWarranty,
) -> Double {
  cohort.population.to_double() +
  cohort.exposure +
  reliability_warranty_cohort_cost(cohort, warranty) +
  reliability_warranty_cohort_claim_rate(cohort) * 100.0
}