///|
/// Evaluate a single flag without named segments.
pub fn Flag::evaluate(self : Flag, ctx : Context) -> Evaluation {
  evaluate_flag_with_segments(self, ctx, Map([]))
}

///|
/// Evaluate a flag from a flag set.
pub fn FlagSet::evaluate(
  self : FlagSet,
  flag_key : StringView,
  ctx : Context,
) -> Evaluation? {
  match self.flags.get(flag_key.to_owned()) {
    Some(flag) => Some(evaluate_flag_with_segments(flag, ctx, self.segments))
    None => None
  }
}

///|
/// Evaluate all flags in a set and return results in insertion order.
pub fn FlagSet::evaluate_all(
  self : FlagSet,
  ctx : Context,
) -> Array[Evaluation] {
  let results : Array[Evaluation] = []
  self.flags.each((_key, flag) => {
    results.push(evaluate_flag_with_segments(flag, ctx, self.segments))
  })
  results
}

///|
/// Deterministically map a user to a rollout bucket in the range `0..<10000`.
pub fn rollout_bucket(
  seed : StringView,
  flag_key : StringView,
  user_key : StringView,
) -> Int {
  let text = seed.to_owned() +
    ":" +
    flag_key.to_owned() +
    ":" +
    user_key.to_owned()
  let mut hash = 2161
  for unit in text.code_units() {
    hash = (hash * 131 + unit.to_int()) % 10000
  }
  hash
}

///|
/// Pick a variant from a rollout for a bucket.
pub fn Rollout::pick(self : Rollout, bucket : Int) -> String? {
  let mut cursor = 0
  for allocation in self.allocations {
    if allocation.weight > 0 {
      cursor = cursor + allocation.weight
      if bucket < cursor {
        return Some(allocation.variant)
      }
    }
  }
  None
}

///|
/// Check whether a rule targets a context.
pub fn Rule::matches(
  self : Rule,
  ctx : Context,
  segments : Map[String, Segment],
) -> Bool {
  if !all_conditions_match(self.conditions, ctx) {
    return false
  }
  for segment_key in self.segments {
    match segments.get(segment_key) {
      Some(segment) => if !segment.matches(ctx) { return false }
      None => return false
    }
  }
  true
}

///|
fn evaluate_flag_with_segments(
  flag : Flag,
  ctx : Context,
  segments : Map[String, Segment],
) -> Evaluation {
  let warnings = missing_segment_warnings(flag, segments)
  if !flag.enabled {
    return evaluation(
      flag_key=flag.key,
      variant=flag.off_variant,
      reason="flag_off",
      matched=false,
      rule_key=None,
      bucket=None,
      warnings~,
    )
  }

  for rule in flag.rules {
    if rule.matches(ctx, segments) {
      return evaluate_rule_result(flag, rule, ctx, warnings)
    }
  }

  match flag.fallthrough_rollout {
    Some(rollout) =>
      match evaluate_rollout(flag.key, rollout, ctx, flag.default_variant) {
        (variant, bucket, rollout_warnings) =>
          evaluation(
            flag_key=flag.key,
            variant~,
            reason="fallthrough_rollout",
            matched=false,
            rule_key=None,
            bucket~,
            warnings=warnings + rollout_warnings,
          )
      }
    None => {
      let variant = flag.fallthrough_variant.unwrap_or(flag.default_variant)
      evaluation(
        flag_key=flag.key,
        variant~,
        reason="fallthrough",
        matched=false,
        rule_key=None,
        bucket=None,
        warnings~,
      )
    }
  }
}

///|
fn evaluate_rule_result(
  flag : Flag,
  rule : Rule,
  ctx : Context,
  warnings : Array[String],
) -> Evaluation {
  match rule.rollout {
    Some(rollout) =>
      match evaluate_rollout(flag.key, rollout, ctx, flag.default_variant) {
        (variant, bucket, rollout_warnings) =>
          evaluation(
            flag_key=flag.key,
            variant~,
            reason=rule.reason,
            matched=true,
            rule_key=Some(rule.key),
            bucket~,
            warnings=warnings + rollout_warnings,
          )
      }
    None => {
      let variant = rule.variant.unwrap_or(flag.default_variant)
      evaluation(
        flag_key=flag.key,
        variant~,
        reason=rule.reason,
        matched=true,
        rule_key=Some(rule.key),
        bucket=None,
        warnings~,
      )
    }
  }
}

///|
fn evaluate_rollout(
  flag_key : String,
  rollout : Rollout,
  ctx : Context,
  default_variant : String,
) -> (String, Int?, Array[String]) {
  match ctx.user_key {
    Some(user_key) => {
      let bucket = rollout_bucket(rollout.seed, flag_key, user_key)
      match rollout.pick(bucket) {
        Some(variant) => (variant, Some(bucket), [])
        None =>
          (
            default_variant,
            Some(bucket),
            ["rollout allocations did not cover the computed bucket"],
          )
      }
    }
    None =>
      (
        default_variant,
        None,
        ["rollout requires Context.user_key; default variant was used"],
      )
  }
}

///|
fn missing_segment_warnings(
  flag : Flag,
  segments : Map[String, Segment],
) -> Array[String] {
  let warnings : Array[String] = []
  for rule in flag.rules {
    for segment_key in rule.segments {
      if !segments.contains(segment_key) {
        warnings.push(
          "rule " + rule.key + " references unknown segment " + segment_key,
        )
      }
    }
  }
  warnings
}

///|
fn evaluation(
  flag_key~ : String,
  variant~ : String,
  reason~ : String,
  matched~ : Bool,
  rule_key~ : String?,
  bucket~ : Int?,
  warnings~ : Array[String],
) -> Evaluation {
  { flag_key, variant, reason, matched, rule_key, bucket, warnings }
}