///|
/// A transparent score contribution from one detector.
pub(all) struct ScoreFactor {
  rule_id : String
  points : Int
  reason : String
}

///|
pub(all) struct RiskScore {
  subject_id : String
  total : Int
  factors : Array[ScoreFactor]
  band : AlertSeverity
}

///|
pub fn ScoreFactor::new(
  rule_id : String,
  points : Int,
  reason : String,
) -> ScoreFactor {
  { rule_id, points, reason }
}

///|
pub fn RiskScore::empty(subject_id : String) -> RiskScore {
  { subject_id, total: 0, factors: [], band: Info }
}

///|
pub fn RiskScore::add(self : RiskScore, factor : ScoreFactor) -> RiskScore {
  let total = self.total + factor.points
  let factors : Array[ScoreFactor] = []
  for old in self.factors {
    factors.push(old)
  }
  factors.push(factor)
  { ..self, total, factors, band: AlertSeverity::from_score(total) }
}

///|
pub fn RiskScore::capped(self : RiskScore, cap : Int) -> RiskScore {
  let total = if self.total > cap { cap } else { self.total }
  { ..self, total, band: AlertSeverity::from_score(total) }
}

///|
pub fn score_alert(alert : Alert) -> RiskScore {
  let score = RiskScore::empty(alert.transaction_id)
  let factor = ScoreFactor::new(
    alert.rule_id,
    10 + alert.evidence.length() * 5,
    "detector matched",
  )
  score.add(factor).capped(100)
}

///|
pub fn aggregate_scores(alerts : Array[Alert]) -> Array[RiskScore] {
  let result : Array[RiskScore] = []
  for alert in alerts {
    let scored = score_alert(alert)
    let mut merged = false
    let mut index = 0
    for old in result {
      if old.subject_id == scored.subject_id {
        result[index] = merge_scores(old, scored)
        merged = true
      }
      index += 1
    }
    if !merged {
      result.push(scored)
    }
  }
  result
}

///|
fn merge_scores(left : RiskScore, right : RiskScore) -> RiskScore {
  let total = if left.total + right.total > 100 {
    100
  } else {
    left.total + right.total
  }
  let factors : Array[ScoreFactor] = []
  for factor in left.factors {
    factors.push(factor)
  }
  for factor in right.factors {
    factors.push(factor)
  }
  {
    subject_id: left.subject_id,
    total,
    factors,
    band: AlertSeverity::from_score(total),
  }
}

///|
pub fn score_rules(
  rules : Array[Rule],
  transactions : Array[Transaction],
) -> Array[RiskScore] {
  aggregate_scores(evaluate(rules, transactions))
}

///|
pub fn highest_score(scores : Array[RiskScore]) -> RiskScore? {
  if scores.length() == 0 {
    None
  } else {
    let mut best = scores[0]
    for score in scores {
      if score.total > best.total {
        best = score
      }
    }
    Some(best)
  }
}