// Copyright 2026 hua1104. Apache-2.0.
//
// Commutation functions and life annuities.
//
// Chinese actuarial practice prices products from commutation columns D, C, M,
// N, R and S built once per (table, interest) pair, after which every net
// single premium and every net annual premium is a single ratio. We build those
// columns eagerly: they cost 106 additions and make subsequent queries O(1).

///|
/// The six classical commutation columns over ages 0 through omega.
pub struct Commutation {
  /// D_x = v^x l_x, the discounted survivors attaining age x.
  d : Array[Double]
  /// C_x = v^(x+1) d_x, the discounted deaths between ages x and x+1.
  c : Array[Double]
  /// M_x = sum of C_k for k >= x.
  m : Array[Double]
  /// N_x = sum of D_k for k >= x.
  n : Array[Double]
  /// R_x = sum of M_k for k >= x.
  r : Array[Double]
  /// S_x = sum of N_k for k >= x.
  s : Array[Double]
}

///|
/// A mortality table paired with an interest assumption. Every actuarial
/// function in this library is a method on `Valuation`, so premiums and
/// reserves computed together always agree on discounting and mortality.
pub struct Valuation {
  table : LifeTable
  interest : Interest
  comm : Commutation
}

///|
/// Build the commutation columns for a table and interest pair.
pub fn Valuation::of(table~ : LifeTable, interest~ : Interest) -> Valuation {
  let size = table.limiting_age() + 1
  let v = interest.discount_factor()
  let d = Array::make(size, 0.0)
  let c = Array::make(size, 0.0)
  let m = Array::make(size, 0.0)
  let n = Array::make(size, 0.0)
  let r = Array::make(size, 0.0)
  let s = Array::make(size, 0.0)
  // Forward pass: powers of v are accumulated rather than exponentiated, which
  // keeps neighbouring ages in exact geometric relation to one another.
  let mut discount = 1.0
  for x in 0.. LifeTable {
  self.table
}

///|
/// The interest assumption this valuation was built from.
pub fn Valuation::interest(self : Valuation) -> Interest {
  self.interest
}

///|
/// The limiting age omega; no life survives past it.
pub fn Valuation::limiting_age(self : Valuation) -> Int32 {
  self.table.limiting_age()
}

///|
/// D_x = v^x l_x.
pub fn Valuation::commutation_d(self : Valuation, x : Int32) -> Double {
  column(self.comm.d, x)
}

///|
/// C_x = v^(x+1) d_x.
pub fn Valuation::commutation_c(self : Valuation, x : Int32) -> Double {
  column(self.comm.c, x)
}

///|
/// M_x = sum of C_k over k >= x.
pub fn Valuation::commutation_m(self : Valuation, x : Int32) -> Double {
  column(self.comm.m, x)
}

///|
/// N_x = sum of D_k over k >= x.
pub fn Valuation::commutation_n(self : Valuation, x : Int32) -> Double {
  column(self.comm.n, x)
}

///|
/// R_x = sum of M_k over k >= x.
pub fn Valuation::commutation_r(self : Valuation, x : Int32) -> Double {
  column(self.comm.r, x)
}

///|
/// S_x = sum of N_k over k >= x.
pub fn Valuation::commutation_s(self : Valuation, x : Int32) -> Double {
  column(self.comm.s, x)
}

///|
/// Read a commutation column at age `x`, treating ages past omega as zero.
fn column(values : Array[Double], x : Int32) -> Double {
  if x < 0 || x >= values.length() {
    0.0
  } else {
    values[x]
  }
}

///|
/// a-double-dot_x: expected present value of a whole life annuity-due paying 1
/// at the start of every year while (x) survives. Equals N_x / D_x.
pub fn Valuation::whole_life_annuity(self : Valuation, x : Int32) -> Double {
  self.commutation_n(x) / self.commutation_d(x)
}

///|
/// a-double-dot_{x:n}: an n-year temporary annuity-due, paying at most n times
/// at the start of each year while (x) survives. Equals (N_x - N_{x+n}) / D_x.
pub fn Valuation::temporary_annuity(
  self : Valuation,
  x : Int32,
  n : Int32,
) -> Double {
  if n <= 0 {
    0.0
  } else {
    (self.commutation_n(x) - self.commutation_n(x + n)) / self.commutation_d(x)
  }
}

///|
/// u|a-double-dot_x: an annuity-due deferred u years, first payment at age
/// x + u. Equals N_{x+u} / D_x.
pub fn Valuation::deferred_annuity(
  self : Valuation,
  x : Int32,
  u : Int32,
) -> Double {
  self.commutation_n(x + u) / self.commutation_d(x)
}

///|
/// a_x: the whole life annuity-immediate, paying at the end of each completed
/// year survived. It is always exactly one payment less than the annuity-due.
pub fn Valuation::whole_life_annuity_immediate(
  self : Valuation,
  x : Int32,
) -> Double {
  self.whole_life_annuity(x) - 1.0
}

///|
/// (Ia-double-dot)_x: increasing whole life annuity-due paying 1 in the first
/// year, 2 in the second, and so on. Equals S_x / D_x.
pub fn Valuation::increasing_annuity(self : Valuation, x : Int32) -> Double {
  self.commutation_s(x) / self.commutation_d(x)
}

///|
/// a-bar_x: a continuous whole life annuity paying at rate 1 per year for as
/// long as (x) survives. Derived from the continuous insurance Ā_x under the
/// UDD assumption using the identity 1 = Ā_x + delta * a-bar_x, with
/// Ā_x = (i / delta) * A_x. Falls back to the discrete annuity when the force
/// of interest degenerates to zero.
pub fn Valuation::continuous_annuity(self : Valuation, x : Int32) -> Double {
  let i_rate = self.interest.annual()
  let delta = self.interest.force()
  if delta == 0.0 {
    self.whole_life_annuity(x)
  } else {
    let continuous_insurance = i_rate / delta * self.whole_life_insurance(x)
    (1.0 - continuous_insurance) / delta
  }
}

///|
/// Check the universal identity 1 = A_x + d * a-double-dot_x, which must hold
/// for any valuation regardless of table or interest rate. Returns the residual
/// rather than a Boolean so a caller can judge its own tolerance.
pub fn Valuation::insurance_annuity_identity(
  self : Valuation,
  x : Int32,
) -> Double {
  let d = self.interest.discount_rate()
  self.whole_life_insurance(x) + d * self.whole_life_annuity(x) - 1.0
}