///|
/// Explainable contribution of one feature to a robust numeric score.
pub struct FeatureAttribution {
  feature : String
  contribution : Double
  absolute_contribution : Double
  direction : String
  rank : Int
}

///|
pub struct AttributionReport {
  baseline : Double
  prediction : Double
  total_contribution : Double
  residual : Double
  features : Array[FeatureAttribution]
}

///|
pub fn feature_attribution(
  feature : String,
  contribution : Double,
  rank : Int,
) -> FeatureAttribution {
  {
    feature,
    contribution,
    absolute_contribution: abs_double(contribution),
    direction: if contribution < 0.0 {
      "negative"
    } else if contribution > 0.0 {
      "positive"
    } else {
      "neutral"
    },
    rank,
  }
}

///|
pub fn attribution_report(
  baseline : Double,
  prediction : Double,
  features : Array[FeatureAttribution],
) -> AttributionReport {
  let mut total = 0.0
  for feature in features {
    total += feature.contribution
  }
  {
    baseline,
    prediction,
    total_contribution: total,
    residual: prediction - baseline - total,
    features,
  }
}

///|
pub fn attribution_from_values(
  names : Array[String],
  values : Array[Double],
  baseline : Double,
  prediction : Double,
) -> AttributionReport {
  let features = []
  let count = if names.length() < values.length() {
    names.length()
  } else {
    values.length()
  }
  for index = 0; index < count; index = index + 1 {
    features.push(feature_attribution(names[index], values[index], index))
  }
  attribution_report(baseline, prediction, features)
}

///|
pub fn attribution_abs_rank(
  report : AttributionReport,
) -> Array[FeatureAttribution] {
  let result = report.features.copy()
  result.sort_by((left, right) => {
    if left.absolute_contribution > right.absolute_contribution {
      -1
    } else if left.absolute_contribution < right.absolute_contribution {
      1
    } else {
      0
    }
  })
  for index = 0; index < result.length(); index = index + 1 {
    result[index] = { ..result[index], rank: index }
  }
  result
}

///|
pub fn attribution_top(
  report : AttributionReport,
  count : Int,
) -> Array[FeatureAttribution] {
  let ranked = attribution_abs_rank(report)
  let result = []
  let width = if count < 0 {
    0
  } else if count > ranked.length() {
    ranked.length()
  } else {
    count
  }
  for index = 0; index < width; index = index + 1 {
    result.push(ranked[index])
  }
  result
}

///|
pub fn attribution_contributions(report : AttributionReport) -> Array[Double] {
  let result = []
  for feature in report.features {
    result.push(feature.contribution)
  }
  result
}

///|
pub fn attribution_names(report : AttributionReport) -> Array[String] {
  let result = []
  for feature in report.features {
    result.push(feature.feature)
  }
  result
}

///|
pub fn attribution_direction_counts(report : AttributionReport) -> Array[Int] {
  let mut positive = 0
  let mut negative = 0
  let mut neutral = 0
  for feature in report.features {
    if feature.direction == "positive" {
      positive += 1
    } else if feature.direction == "negative" {
      negative += 1
    } else {
      neutral += 1
    }
  }
  [positive, negative, neutral]
}

///|
pub fn attribution_share(report : AttributionReport) -> Array[Double] {
  let total = sum_absolute(attribution_contributions(report))
  let result = []
  for feature in report.features {
    result.push(
      if total == 0.0 {
        0.0
      } else {
        feature.absolute_contribution / total
      },
    )
  }
  result
}

///|
pub fn attribution_reconstruct(report : AttributionReport) -> Double {
  report.baseline + report.total_contribution + report.residual
}

///|
pub fn attribution_add(
  report : AttributionReport,
  feature : FeatureAttribution,
) -> AttributionReport {
  let features = report.features.copy()
  features.push(feature)
  attribution_report(report.baseline, report.prediction, features)
}

///|
pub fn attribution_merge(
  left : AttributionReport,
  right : AttributionReport,
) -> AttributionReport {
  let features = left.features.copy()
  for feature in right.features {
    features.push(feature)
  }
  attribution_report(left.baseline, right.prediction, features)
}

///|
pub fn attribution_from_linear(
  feature_names : Array[String],
  coefficients : Array[Double],
  feature_values : Array[Double],
  intercept : Double,
) -> AttributionReport {
  let contributions = []
  let mut prediction = intercept
  let count = if coefficients.length() < feature_values.length() {
    coefficients.length()
  } else {
    feature_values.length()
  }
  for index = 0; index < count; index = index + 1 {
    let value = coefficients[index] * feature_values[index]
    contributions.push(value)
    prediction += value
  }
  attribution_from_values(feature_names, contributions, intercept, prediction)
}

///|
pub fn attribution_residual_quality(report : AttributionReport) -> Double {
  1.0 / (1.0 + abs_double(report.residual))
}

///|
pub fn attribution_consistency(report : AttributionReport) -> Double {
  let reconstructed = attribution_reconstruct(report)
  1.0 / (1.0 + abs_double(reconstructed - report.prediction))
}

///|
pub fn attribution_report_vector(report : AttributionReport) -> Array[Double] {
  [
    report.baseline,
    report.prediction,
    report.total_contribution,
    report.residual,
    report.features.length().to_double(),
    attribution_residual_quality(report),
    attribution_consistency(report),
  ]
}

///|
pub fn attribution_lines(report : AttributionReport) -> Array[String] {
  let lines = [
    "baseline=" + report.baseline.to_string(),
    "prediction=" + report.prediction.to_string(),
    "total_contribution=" + report.total_contribution.to_string(),
    "residual=" + report.residual.to_string(),
  ]
  for feature in attribution_abs_rank(report) {
    lines.push(
      feature.feature +
      "|" +
      feature.contribution.to_string() +
      "|" +
      feature.direction +
      "|rank=" +
      feature.rank.to_string(),
    )
  }
  lines
}

///|
pub fn attribution_string(report : AttributionReport) -> String {
  attribution_lines(report).join("\n")
}

///|
pub fn attribution_stable(
  left : AttributionReport,
  right : AttributionReport,
  tolerance : Double,
) -> Bool {
  let left_ranked = attribution_abs_rank(left)
  let right_ranked = attribution_abs_rank(right)
  if left_ranked.length() != right_ranked.length() {
    return false
  }
  for index = 0; index < left_ranked.length(); index = index + 1 {
    if left_ranked[index].feature != right_ranked[index].feature ||
      abs_double(
        left_ranked[index].contribution - right_ranked[index].contribution,
      ) >
      tolerance {
      return false
    }
  }
  true
}

///|
pub fn attribution_sensitivity(
  base : Array[Double],
  changed : Array[Double],
  names : Array[String],
) -> AttributionReport {
  let count = if base.length() < changed.length() {
    base.length()
  } else {
    changed.length()
  }
  let values = []
  for index = 0; index < count; index = index + 1 {
    values.push(changed[index] - base[index])
  }
  let mut base_total = 0.0
  let mut changed_total = 0.0
  for value in base {
    base_total += value
  }
  for value in changed {
    changed_total += value
  }
  attribution_from_values(names, values, base_total, changed_total)
}

///|
pub fn attribution_batch(reports : Array[AttributionReport]) -> Array[Double] {
  let result = []
  for report in reports {
    result.push(attribution_residual_quality(report))
  }
  result
}

///|
pub fn attribution_summary(report : AttributionReport) -> Array[Double] {
  let directions = attribution_direction_counts(report)
  [
    report.prediction,
    report.residual,
    report.features.length().to_double(),
    directions[0].to_double(),
    directions[1].to_double(),
    directions[2].to_double(),
    attribution_consistency(report),
  ]
}