///|
/// Warranty policy and renewal-cost analysis.
pub struct WarrantyPolicy {
  duration : Double
  replacement_cost : Double
  service_cost : Double
  salvage_value : Double
  renewal : Bool
}

///|
pub fn warranty_policy(
  duration~ : Double,
  replacement_cost~ : Double,
  service_cost~ : Double,
  salvage_value~ : Double,
  renewal~ : Bool,
) -> WarrantyPolicy {
  if duration <= 0.0 || replacement_cost < 0.0 || service_cost < 0.0 {
    abort("invalid warranty policy")
  }
  { duration, replacement_cost, service_cost, salvage_value, renewal }
}

///|
pub struct WarrantyAnalysis {
  claims : Double
  expected_cost : Double
  cost_per_unit_time : Double
  claim_probability : Double
  renewal_cycles : Double
}

///|
pub fn warranty_analysis(
  claims~ : Double,
  expected_cost~ : Double,
  cost_per_unit_time~ : Double,
  claim_probability~ : Double,
  renewal_cycles~ : Double,
) -> WarrantyAnalysis {
  {
    claims,
    expected_cost,
    cost_per_unit_time,
    claim_probability,
    renewal_cycles,
  }
}

///|
pub fn expected_warranty_claims(
  model : ReliabilityModel,
  policy : WarrantyPolicy,
) -> Double {
  model.cdf(policy.duration)
}

///|
pub fn expected_warranty_cost(
  model : ReliabilityModel,
  policy : WarrantyPolicy,
) -> Double {
  expected_warranty_claims(model, policy) *
  (policy.replacement_cost + policy.service_cost) -
  policy.salvage_value * model.survival(policy.duration)
}

///|
pub fn analyze_warranty(
  model : ReliabilityModel,
  policy : WarrantyPolicy,
) -> WarrantyAnalysis {
  let claim_probability = expected_warranty_claims(model, policy)
  let cost = expected_warranty_cost(model, policy)
  let cycles = if policy.renewal {
    1.0 / model.survival(policy.duration).max(1.0e-12)
  } else {
    1.0
  }
  warranty_analysis(
    claims=claim_probability * cycles,
    expected_cost=cost * cycles,
    cost_per_unit_time=cost * cycles / (policy.duration * cycles),
    claim_probability~,
    renewal_cycles=cycles,
  )
}

///|
pub fn warranty_replacement_threshold(
  policy : WarrantyPolicy,
  administrative_cost : Double,
) -> Double {
  if policy.replacement_cost <= policy.salvage_value {
    0.0
  } else {
    administrative_cost / (policy.replacement_cost - policy.salvage_value)
  }
}

///|
pub fn preventive_replacement_cost(
  model : ReliabilityModel,
  age : Double,
  replacement_cost : Double,
) -> Double {
  if age <= 0.0 {
    abort("replacement age must be positive")
  }
  let failure_cost = replacement_cost * model.cdf(age)
  failure_cost / age
}

///|
pub fn optimize_replacement_age(
  model : ReliabilityModel,
  minimum_age : Double,
  maximum_age : Double,
  steps : Int,
  replacement_cost : Double,
) -> Double {
  let grid = linspace(minimum_age, maximum_age, steps)
  let costs = grid.map(age => {
    preventive_replacement_cost(model, age, replacement_cost)
  })
  let mut best = 0
  for i in 1..