///|
/// A deterministic, explainable AML rules engine.
pub fn evaluate(
  rules : Array[Rule],
  transactions : Array[Transaction],
) -> Array[Alert] {
  let alerts = []
  for transaction in transactions {
    for rule in rules {
      match evaluate_rule(rule, transaction, transactions) {
        Some(alert) => alerts.push(alert)
        None => ()
      }
    }
  }
  alerts
}

///|
fn evaluate_rule(
  rule : Rule,
  transaction : Transaction,
  all : Array[Transaction],
) -> Alert? {
  match rule {
    AmountAtLeast(id, minimum) =>
      if transaction.amount >= minimum {
        Some(
          Alert::new(id, transaction.id, [
            "amount=\{transaction.amount} >= \{minimum}",
          ]),
        )
      } else {
        None
      }
    AmountAtMost(id, maximum) =>
      if transaction.amount <= maximum {
        Some(
          Alert::new(id, transaction.id, [
            "amount=\{transaction.amount} <= \{maximum}",
          ]),
        )
      } else {
        None
      }
    CustomerFrequency(id, minimum, window) => {
      let count = count_customer_in_window(transaction, all, window)
      if count >= minimum {
        Some(
          Alert::new(id, transaction.id, [
            "customer=\{transaction.customer_id} count=\{count} window=\{window}s",
          ]),
        )
      } else {
        None
      }
    }
    AccountFrequency(id, minimum, window) => {
      let count = count_account_in_window(transaction, all, window)
      if count >= minimum {
        Some(
          Alert::new(id, transaction.id, [
            "account=\{transaction.account_id} count=\{count} window=\{window}s",
          ]),
        )
      } else {
        None
      }
    }
    CustomerVolume(id, minimum, window) => {
      let total = customer_volume_in_window(transaction, all, window)
      if total >= minimum {
        Some(
          Alert::new(id, transaction.id, [
            "customer=\{transaction.customer_id} volume=\{total} window=\{window}s",
          ]),
        )
      } else {
        None
      }
    }
    RegionIs(id, region) =>
      if transaction.region == region {
        Some(Alert::new(id, transaction.id, ["region=\{region}"]))
      } else {
        None
      }
    CurrencyIs(id, currency) =>
      if transaction.currency == currency {
        Some(Alert::new(id, transaction.id, ["currency=\{currency}"]))
      } else {
        None
      }
    RapidMovement(id, amount, window) => {
      let total = customer_volume_in_window(transaction, all, window)
      if total >= amount {
        Some(
          Alert::new(id, transaction.id, [
            "rapid_volume=\{total} window=\{window}s",
          ]),
        )
      } else {
        None
      }
    }
    Structuring(id, unit, minimum, window) => {
      let mut count = 0
      for candidate in all {
        let delta = transaction.occurred_at - candidate.occurred_at
        if candidate.customer_id == transaction.customer_id &&
          candidate.amount >= unit / 2 &&
          candidate.amount < unit &&
          delta >= 0 &&
          delta <= window {
          count += 1
        }
      }
      if count >= minimum {
        Some(Alert::new(id, transaction.id, ["structuring_count=\{count}"]))
      } else {
        None
      }
    }
  }
}

///|
pub fn count_customer_in_window(
  tx : Transaction,
  all : Array[Transaction],
  window : Int,
) -> Int {
  let mut count = 0
  for candidate in all {
    let delta = tx.occurred_at - candidate.occurred_at
    if candidate.customer_id == tx.customer_id && delta >= 0 && delta <= window {
      count += 1
    }
  }
  count
}

///|
pub fn count_account_in_window(
  tx : Transaction,
  all : Array[Transaction],
  window : Int,
) -> Int {
  let mut count = 0
  for candidate in all {
    let delta = tx.occurred_at - candidate.occurred_at
    if candidate.account_id == tx.account_id && delta >= 0 && delta <= window {
      count += 1
    }
  }
  count
}

///|
pub fn customer_volume_in_window(
  tx : Transaction,
  all : Array[Transaction],
  window : Int,
) -> Int {
  let mut total = 0
  for candidate in all {
    let delta = tx.occurred_at - candidate.occurred_at
    if candidate.customer_id == tx.customer_id && delta >= 0 && delta <= window {
      total += candidate.amount
    }
  }
  total
}

///|
pub fn evaluate_validated(
  rules : Array[Rule],
  transactions : Array[Transaction],
) -> (Array[Alert], Array[ValidationIssue]) {
  let normalized = []
  for tx in transactions {
    normalized.push(normalize_transaction(tx))
  }
  (evaluate(rules, normalized), validate_batch(normalized))
}

///|
pub fn deduplicate_alerts(alerts : Array[Alert]) -> Array[Alert] {
  let result : Array[Alert] = []
  for alert in alerts {
    let mut duplicate = false
    for old in result {
      if old.rule_id == alert.rule_id &&
        old.transaction_id == alert.transaction_id {
        duplicate = true
      }
    }
    if !duplicate {
      result.push(alert)
    }
  }
  result
}