///|
/// Robust risk snapshot for a return series.
pub struct RiskSnapshot {
  count : Int
  center : Double
  scale : Double
  downside : Double
  upside : Double
  value_at_risk : Double
  conditional_var : Double
  maximum_drawdown : Double
  sharpe : Double
  sortino : Double
  tail_ratio : Double
}

///|
/// One completed drawdown episode.
pub struct DrawdownEpisode {
  start : Int
  trough : Int
  recovery : Int
  depth : Double
  duration : Int
  recovered : Bool
}

///|
/// Scenario configuration for stress analysis.
pub struct RiskRule {
  confidence : Double
  target : Double
  risk_free : Double
  stress_scale : Double
}

///|
pub fn risk_default_rule() -> RiskRule {
  { confidence: 0.95, target: 0.0, risk_free: 0.0, stress_scale: 2.0 }
}

///|
pub fn risk_rule(
  confidence : Double,
  target : Double,
  risk_free : Double,
  stress_scale : Double,
) -> RiskRule {
  {
    confidence: if confidence <= 0.0 {
      0.5
    } else if confidence >= 1.0 {
      0.999
    } else {
      confidence
    },
    target,
    risk_free,
    stress_scale: if stress_scale < 0.0 {
      -stress_scale
    } else {
      stress_scale
    },
  }
}

///|
pub fn risk_returns(prices : Array[Double]) -> Array[Double] {
  let result = []
  for index = 1; index < prices.length(); index = index + 1 {
    if abs_double(prices[index - 1]) <= 1.0e-12 {
      result.push(0.0)
    } else {
      result.push(
        (prices[index] - prices[index - 1]) / abs_double(prices[index - 1]),
      )
    }
  }
  result
}

///|
pub fn risk_log_returns(prices : Array[Double]) -> Array[Double] {
  let result = []
  for index = 1; index < prices.length(); index = index + 1 {
    if prices[index] <= 0.0 || prices[index - 1] <= 0.0 {
      result.push(0.0)
    } else {
      result.push(drift_log(prices[index]) - drift_log(prices[index - 1]))
    }
  }
  result
}

///|
pub fn risk_cumulative_returns(returns : Array[Double]) -> Array[Double] {
  let result = []
  let mut wealth = 1.0
  for value in returns {
    wealth *= 1.0 + value
    result.push(wealth - 1.0)
  }
  result
}

///|
pub fn risk_wealth_curve(
  returns : Array[Double],
  initial : Double,
) -> Array[Double] {
  let result = []
  let mut wealth = initial
  for value in returns {
    wealth *= 1.0 + value
    result.push(wealth)
  }
  result
}

///|
pub fn risk_annualized_return(returns : Array[Double], periods : Int) -> Double {
  if returns.length() == 0 || periods <= 0 {
    return 0.0
  }
  mean(returns) * periods.to_double()
}

///|
pub fn risk_annualized_volatility(
  returns : Array[Double],
  periods : Int,
) -> Double {
  if returns.length() == 0 || periods <= 0 {
    0.0
  } else {
    sample_stddev(returns) * periods.to_double().sqrt()
  }
}

///|
pub fn risk_downside(returns : Array[Double], target : Double) -> Double {
  downside_deviation(returns, target)
}

///|
pub fn risk_upside(returns : Array[Double], target : Double) -> Double {
  upside_deviation(returns, target)
}

///|
pub fn risk_lower_tail(
  returns : Array[Double],
  probability : Double,
) -> Array[Double] {
  let threshold = quantile(returns, probability)
  let result = []
  for value in returns {
    if value <= threshold {
      result.push(value)
    }
  }
  result
}

///|
pub fn risk_upper_tail(
  returns : Array[Double],
  probability : Double,
) -> Array[Double] {
  let threshold = quantile(returns, probability)
  let result = []
  for value in returns {
    if value >= threshold {
      result.push(value)
    }
  }
  result
}

///|
pub fn risk_var(returns : Array[Double], confidence : Double) -> Double {
  value_at_risk(returns, confidence)
}

///|
pub fn risk_cvar(returns : Array[Double], confidence : Double) -> Double {
  conditional_value_at_risk(returns, confidence)
}

///|
pub fn risk_expected_shortfall(
  returns : Array[Double],
  confidence : Double,
) -> Double {
  expected_shortfall(returns, confidence)
}

///|
pub fn risk_tail_ratio(returns : Array[Double], probability : Double) -> Double {
  let lower = abs_double(quantile(returns, 1.0 - probability))
  let upper = quantile(returns, probability)
  if lower <= 1.0e-12 {
    0.0
  } else {
    upper / lower
  }
}

///|
pub fn risk_drawdown_series(wealth : Array[Double]) -> Array[Double] {
  drawdown_series(wealth)
}

///|
pub fn risk_recovery_series(wealth : Array[Double]) -> Array[Double] {
  let drawdowns = drawdown_series(wealth)
  let result = []
  let mut peak = 0.0
  for index = 0; index < wealth.length(); index = index + 1 {
    if wealth[index] > peak {
      peak = wealth[index]
    }
    result.push(if peak == 0.0 { 0.0 } else { wealth[index] / peak })
  }
  ignore(drawdowns)
  result
}

///|
pub fn risk_maximum_drawdown(wealth : Array[Double]) -> Double {
  maximum_drawdown(wealth)
}

///|
pub fn risk_drawdown_duration(wealth : Array[Double]) -> Array[Int] {
  let result = []
  let drawdowns = drawdown_series(wealth)
  let duration = 0
  let mut current = duration
  for value in drawdowns {
    if value < 0.0 {
      current += 1
    } else {
      current = 0
    }
    result.push(current)
  }
  result
}

///|
pub fn risk_drawdown_episodes(wealth : Array[Double]) -> Array[DrawdownEpisode] {
  let result = []
  let drawdowns = drawdown_series(wealth)
  let index = 0
  let mut cursor = index
  while cursor < drawdowns.length() {
    if drawdowns[cursor] >= 0.0 {
      cursor += 1
    } else {
      let start = cursor
      let mut trough = cursor
      let mut end = cursor
      while end + 1 < drawdowns.length() && drawdowns[end + 1] < 0.0 {
        end += 1
        if drawdowns[end] < drawdowns[trough] {
          trough = end
        }
      }
      let mut recovery = end + 1
      while recovery < drawdowns.length() && drawdowns[recovery] < 0.0 {
        recovery += 1
      }
      let recovered = recovery < drawdowns.length()
      result.push({
        start,
        trough,
        recovery: if recovered {
          recovery
        } else {
          -1
        },
        depth: drawdowns[trough],
        duration: if recovered {
          recovery - start
        } else {
          drawdowns.length() - start
        },
        recovered,
      })
      cursor = if recovered { recovery } else { drawdowns.length() }
    }
  }
  result
}

///|
pub fn risk_recovery_rate(wealth : Array[Double]) -> Double {
  let episodes = risk_drawdown_episodes(wealth)
  if episodes.length() == 0 {
    1.0
  } else {
    let mut recovered = 0
    for episode in episodes {
      if episode.recovered {
        recovered += 1
      }
    }
    recovered.to_double() / episodes.length().to_double()
  }
}

///|
pub fn risk_sharpe(returns : Array[Double], risk_free : Double) -> Double {
  robust_sharpe_ratio(returns, risk_free~)
}

///|
pub fn risk_sortino(returns : Array[Double], target : Double) -> Double {
  robust_sortino_ratio(returns, target~)
}

///|
pub fn risk_calmar(returns : Array[Double], risk_free : Double) -> Double {
  let wealth = risk_wealth_curve(returns, 1.0)
  let annual = risk_annualized_return(returns, 252)
  let drawdown = abs_double(risk_maximum_drawdown(wealth))
  if drawdown <= 1.0e-12 {
    annual - risk_free
  } else {
    (annual - risk_free) / drawdown
  }
}

///|
pub fn risk_snapshot(returns : Array[Double], rule : RiskRule) -> RiskSnapshot {
  let wealth = risk_wealth_curve(returns, 1.0)
  {
    count: returns.length(),
    center: median(returns),
    scale: mad(returns) * 1.4826,
    downside: risk_downside(returns, rule.target),
    upside: risk_upside(returns, rule.target),
    value_at_risk: risk_var(returns, rule.confidence),
    conditional_var: risk_cvar(returns, rule.confidence),
    maximum_drawdown: risk_maximum_drawdown(wealth),
    sharpe: risk_sharpe(returns, rule.risk_free),
    sortino: risk_sortino(returns, rule.target),
    tail_ratio: risk_tail_ratio(returns, 1.0 - (1.0 - rule.confidence) / 2.0),
  }
}

///|
pub fn risk_snapshot_vector(snapshot : RiskSnapshot) -> Array[Double] {
  [
    snapshot.count.to_double(),
    snapshot.center,
    snapshot.scale,
    snapshot.downside,
    snapshot.upside,
    snapshot.value_at_risk,
    snapshot.conditional_var,
    snapshot.maximum_drawdown,
    snapshot.sharpe,
    snapshot.sortino,
    snapshot.tail_ratio,
  ]
}

///|
pub fn risk_snapshot_lines(snapshot : RiskSnapshot) -> Array[String] {
  [
    "count=" + snapshot.count.to_string(),
    "center=" + snapshot.center.to_string(),
    "scale=" + snapshot.scale.to_string(),
    "downside=" + snapshot.downside.to_string(),
    "upside=" + snapshot.upside.to_string(),
    "value_at_risk=" + snapshot.value_at_risk.to_string(),
    "conditional_var=" + snapshot.conditional_var.to_string(),
    "maximum_drawdown=" + snapshot.maximum_drawdown.to_string(),
    "sharpe=" + snapshot.sharpe.to_string(),
    "sortino=" + snapshot.sortino.to_string(),
    "tail_ratio=" + snapshot.tail_ratio.to_string(),
  ]
}

///|
pub fn risk_snapshot_string(snapshot : RiskSnapshot) -> String {
  risk_snapshot_lines(snapshot).join("\n")
}

///|
pub fn risk_stress_returns(
  returns : Array[Double],
  shock : Double,
) -> Array[Double] {
  let result = []
  for value in returns {
    result.push(value - shock)
  }
  result
}

///|
pub fn risk_scale_stress(
  returns : Array[Double],
  scale : Double,
) -> Array[Double] {
  let result = []
  for value in returns {
    result.push(value * scale)
  }
  result
}

///|
pub fn risk_reverse_stress(returns : Array[Double]) -> Array[Double] {
  let result = []
  for value in returns {
    result.push(-value)
  }
  result
}

///|
pub fn risk_stress_table(
  returns : Array[Double],
  shocks : Array[Double],
) -> Array[Array[Double]] {
  let result = []
  for shock in shocks {
    let stressed = risk_stress_returns(returns, shock)
    let snapshot = risk_snapshot(stressed, risk_default_rule())
    result.push([
      shock,
      snapshot.value_at_risk,
      snapshot.conditional_var,
      snapshot.maximum_drawdown,
      snapshot.sharpe,
    ])
  }
  result
}

///|
pub fn risk_rolling_snapshot_score(
  returns : Array[Double],
  window : Int,
  rule : RiskRule,
) -> Array[Double] {
  let result = []
  if window <= 0 {
    return result
  }
  for end = window; end <= returns.length(); end = end + 1 {
    let values = []
    for index = end - window; index < end; index = index + 1 {
      values.push(returns[index])
    }
    let snapshot = risk_snapshot(values, rule)
    result.push(
      abs_double(snapshot.value_at_risk) + abs_double(snapshot.maximum_drawdown),
    )
  }
  result
}

///|
pub fn risk_rolling_var(
  returns : Array[Double],
  window : Int,
  confidence : Double,
) -> Array[Double] {
  let result = []
  if window <= 0 {
    return result
  }
  for end = window; end <= returns.length(); end = end + 1 {
    let values = []
    for index = end - window; index < end; index = index + 1 {
      values.push(returns[index])
    }
    result.push(risk_var(values, confidence))
  }
  result
}

///|
pub fn risk_rolling_drawdown(
  returns : Array[Double],
  window : Int,
) -> Array[Double] {
  let result = []
  if window <= 0 {
    return result
  }
  for end = window; end <= returns.length(); end = end + 1 {
    let values = []
    for index = end - window; index < end; index = index + 1 {
      values.push(returns[index])
    }
    result.push(risk_maximum_drawdown(risk_wealth_curve(values, 1.0)))
  }
  result
}

///|
pub fn risk_contribution(
  component_returns : Array[Array[Double]],
  weights : Array[Double],
) -> Array[Double] {
  let result = []
  let total_weight = sum_absolute(weights)
  for index = 0; index < component_returns.length(); index = index + 1 {
    let contribution = if index < weights.length() {
      abs_double(weights[index]) *
      risk_snapshot(component_returns[index], risk_default_rule()).scale
    } else {
      0.0
    }
    result.push(
      if total_weight == 0.0 {
        0.0
      } else {
        contribution / total_weight
      },
    )
  }
  result
}

///|
pub fn risk_portfolio_return(
  component_returns : Array[Array[Double]],
  weights : Array[Double],
) -> Array[Double] {
  let result = []
  if component_returns.length() == 0 {
    return result
  }
  let length = component_returns[0].length()
  for time = 0; time < length; time = time + 1 {
    let mut value = 0.0
    for component = 0
        component < component_returns.length()
        component = component + 1 {
      if component < weights.length() &&
        time < component_returns[component].length() {
        value += component_returns[component][time] * weights[component]
      }
    }
    result.push(value)
  }
  result
}

///|
pub fn risk_portfolio_snapshot(
  component_returns : Array[Array[Double]],
  weights : Array[Double],
  rule : RiskRule,
) -> RiskSnapshot {
  risk_snapshot(risk_portfolio_return(component_returns, weights), rule)
}

///|
pub fn risk_concentration(weights : Array[Double]) -> Double {
  let total = sum_absolute(weights)
  if total == 0.0 {
    0.0
  } else {
    sum_squared(weights) / (total * total)
  }
}

///|
pub fn risk_effective_number(weights : Array[Double]) -> Double {
  let concentration = risk_concentration(weights)
  if concentration <= 1.0e-12 {
    0.0
  } else {
    1.0 / concentration
  }
}

///|
pub fn risk_diversification_score(weights : Array[Double]) -> Double {
  let number = risk_effective_number(weights)
  if weights.length() == 0 {
    0.0
  } else {
    number / weights.length().to_double()
  }
}

///|
pub fn risk_return_quality(returns : Array[Double], rule : RiskRule) -> Double {
  let snapshot = risk_snapshot(returns, rule)
  (snapshot.sharpe + snapshot.sortino + snapshot.tail_ratio) / 3.0
}

///|
pub fn risk_stress_sensitivity(
  returns : Array[Double],
  shocks : Array[Double],
  rule : RiskRule,
) -> Array[Double] {
  let base = risk_snapshot(returns, rule).value_at_risk
  let result = []
  for shock in shocks {
    result.push(
      risk_snapshot(risk_stress_returns(returns, shock), rule).value_at_risk -
      base,
    )
  }
  result
}