///|
/// Decision analysis, threshold policies, and cost-sensitive evaluation.
pub struct DecisionCost {
  false_positive : Double
  false_negative : Double
  true_positive : Double
  true_negative : Double
} derive(Debug, Eq, ToJson, FromJson)

///|
pub fn DecisionCost::new(
  false_positive : Double,
  false_negative : Double,
) -> DecisionCost {
  if false_positive < 0.0 || false_negative < 0.0 {
    abort("DecisionCost: costs must be non-negative")
  }
  { false_positive, false_negative, true_positive: 0.0, true_negative: 0.0 }
}

///|
pub fn DecisionCost::expected(
  self : DecisionCost,
  probabilities : Array[Double],
  actual : Array[Bool],
  threshold : Double,
) -> Double {
  if probabilities.length() != actual.length() ||
    probabilities.length() == 0 ||
    threshold < 0.0 ||
    threshold > 1.0 {
    abort("DecisionCost: invalid inputs")
  }
  let mut total = 0.0
  for index in 0..= threshold
    if predicted && !actual[index] {
      total = total + self.false_positive
    } else if !predicted && actual[index] {
      total = total + self.false_negative
    }
  }
  total / actual.length().to_double()
}

///|
pub struct ThresholdReport {
  threshold : Double
  expected_cost : Double
  accuracy : Double
  precision : Double
  recall : Double
  f1 : Double
  selected_rate : Double
} derive(Debug, Eq, ToJson, FromJson)

///|
pub fn threshold_report(
  probabilities : Array[Double],
  actual : Array[Bool],
  threshold : Double,
  cost : DecisionCost,
) -> ThresholdReport {
  if probabilities.length() != actual.length() ||
    probabilities.length() == 0 ||
    threshold < 0.0 ||
    threshold > 1.0 {
    abort("threshold_report: invalid inputs")
  }
  let mut tp = 0
  let mut fp = 0
  let mut fn_count = 0
  let mut correct = 0
  let mut selected = 0
  for index in 0..= threshold
    if predicted {
      selected = selected + 1
    }
    if predicted && actual[index] {
      tp = tp + 1
    } else if predicted && !actual[index] {
      fp = fp + 1
    } else if !predicted && actual[index] {
      fn_count = fn_count + 1
    }
    if predicted == actual[index] {
      correct = correct + 1
    }
  }
  let precision = if tp + fp == 0 {
    0.0
  } else {
    tp.to_double() / (tp + fp).to_double()
  }
  let recall = if tp + fn_count == 0 {
    0.0
  } else {
    tp.to_double() / (tp + fn_count).to_double()
  }
  let f1 = if precision + recall == 0.0 {
    0.0
  } else {
    2.0 * precision * recall / (precision + recall)
  }
  {
    threshold,
    expected_cost: cost.expected(probabilities, actual, threshold),
    accuracy: correct.to_double() / actual.length().to_double(),
    precision,
    recall,
    f1,
    selected_rate: selected.to_double() / actual.length().to_double(),
  }
}

///|
pub fn optimize_threshold(
  probabilities : Array[Double],
  actual : Array[Bool],
  cost : DecisionCost,
  steps : Int,
) -> ThresholdReport {
  if steps <= 0 {
    abort("optimize_threshold: steps must be positive")
  }
  let mut best = threshold_report(probabilities, actual, 0.0, cost)
  for index in 1..<=steps {
    let candidate = threshold_report(
      probabilities,
      actual,
      index.to_double() / steps.to_double(),
      cost,
    )
    if candidate.expected_cost < best.expected_cost {
      best = candidate
    }
  }
  best
}

///|
pub struct DecisionConfusionMatrix {
  true_positive : Int
  false_positive : Int
  true_negative : Int
  false_negative : Int
} derive(Debug, Eq, ToJson, FromJson)

///|
pub fn confusion_matrix(
  predicted : Array[Bool],
  actual : Array[Bool],
) -> DecisionConfusionMatrix {
  if predicted.length() != actual.length() {
    abort("confusion_matrix: lengths must match")
  }
  let mut tp = 0
  let mut fp = 0
  let mut tn = 0
  let mut fn_count = 0
  for index in 0.. Double {
  matrix.false_positive.to_double() * cost.false_positive +
  matrix.false_negative.to_double() * cost.false_negative
}

///|
pub fn balanced_accuracy(matrix : DecisionConfusionMatrix) -> Double {
  let sensitivity = if matrix.true_positive + matrix.false_negative == 0 {
    0.0
  } else {
    matrix.true_positive.to_double() /
    (matrix.true_positive + matrix.false_negative).to_double()
  }
  let specificity = if matrix.true_negative + matrix.false_positive == 0 {
    0.0
  } else {
    matrix.true_negative.to_double() /
    (matrix.true_negative + matrix.false_positive).to_double()
  }
  (sensitivity + specificity) / 2.0
}

///|
pub struct UtilityCurvePoint {
  threshold : Double
  net_benefit : Double
  treated : Int
  true_positives : Int
} derive(Debug, Eq, ToJson, FromJson)

///|
pub fn utility_curve(
  probabilities : Array[Double],
  actual : Array[Bool],
  harm : Double,
  steps : Int,
) -> Array[UtilityCurvePoint] {
  if probabilities.length() != actual.length() ||
    probabilities.length() == 0 ||
    harm < 0.0 ||
    steps <= 0 {
    abort("utility_curve: invalid inputs")
  }
  let result : Array[UtilityCurvePoint] = []
  for step in 0..<=steps {
    let threshold = step.to_double() / steps.to_double()
    let mut treated = 0
    let mut true_positives = 0
    for index in 0..= threshold {
        treated = treated + 1
        if actual[index] {
          true_positives = true_positives + 1
        }
      }
    }
    let benefit = true_positives.to_double() / actual.length().to_double()
    let burden = (treated - true_positives).to_double() /
      actual.length().to_double() *
      harm
    result.push({
      threshold,
      net_benefit: benefit - burden,
      treated,
      true_positives,
    })
  }
  result
}

///|
pub enum Action {
  Reject
  Review
  Accept
  Escalate
} derive(Debug, Eq, ToJson, FromJson)

///|
pub struct ActionRule {
  lower : Double
  upper : Double
  action : Action
  priority : Int
} derive(Debug, Eq, ToJson, FromJson)

///|
pub fn ActionRule::matches(self : ActionRule, score : Double) -> Bool {
  score >= self.lower && score < self.upper
}

///|
pub fn choose_action(
  score : Double,
  rules : Array[ActionRule],
  fallback : Action,
) -> Action {
  let mut chosen = fallback
  let mut priority = -1
  for rule in rules.iter() {
    if rule.matches(score) && rule.priority >= priority {
      chosen = rule.action
      priority = rule.priority
    }
  }
  chosen
}

///|
pub struct DecisionLog {
  id : String
  score : Double
  action : Action
  threshold : Double
  reason : String
  timestamp : Int
} derive(Debug, Eq, ToJson, FromJson)

///|
pub fn DecisionLog::new(
  id : String,
  score : Double,
  action : Action,
  threshold : Double,
  timestamp : Int,
) -> DecisionLog {
  { id, score, action, threshold, reason: "", timestamp }
}

///|
pub fn decision_consistency(logs : Array[DecisionLog]) -> Double {
  if logs.length() == 0 {
    return 1.0
  }
  let mut consistent = 0
  for log in logs.iter() {
    if (log.score >= log.threshold && log.action != Action::Reject) ||
      (log.score < log.threshold && log.action == Action::Reject) {
      consistent = consistent + 1
    }
  }
  consistent.to_double() / logs.length().to_double()
}

///|
pub fn expected_value(
  probability : Double,
  benefit : Double,
  cost : Double,
) -> Double {
  probability * benefit - (1.0 - probability) * cost
}

///|
pub fn break_even_probability(benefit : Double, cost : Double) -> Double {
  if benefit + cost == 0.0 {
    0.5
  } else {
    cost / (benefit + cost)
  }
}

///|
pub fn value_of_information(
  current : Double,
  improved : Double,
  information_cost : Double,
) -> Double {
  improved - current - information_cost
}