///|
fn clamp_percentage(value : Int) -> Int {
  if value < 0 {
    0
  } else if value > 10000 {
    10000
  } else {
    value
  }
}

///|
pub fn rollout_bucket(flag_key : String, targeting_key : String) -> Int {
  let source = flag_key + ":" + targeting_key
  let mut acc = 5381
  for char in source {
    acc = (acc * 33 + char.to_int()) % 10000
  }
  acc
}

///|
pub fn rollout_matches(
  flag_key : String,
  targeting_key : String,
  percentage : Int,
) -> Bool {
  rollout_bucket(flag_key, targeting_key) < clamp_percentage(percentage)
}

///|
pub fn rollout_percentage_for(flag_key : String, targeting_key : String) -> Int {
  rollout_bucket(flag_key, targeting_key)
}

///|
pub fn rollout_buckets(
  flag_key : String,
  targeting_keys : Array[String],
) -> Array[Int] {
  targeting_keys.map(targeting_key => rollout_bucket(flag_key, targeting_key))
}

///|
pub(all) struct RolloutDistribution {
  samples : Int
  matched : Int
  requested_percentage : Int
  observed_percentage : Double
  min_bucket : Int
  max_bucket : Int
} derive(Debug, Eq)

///|
pub fn measure_rollout(
  flag_key : String,
  targeting_keys : Array[String],
  percentage : Int,
) -> RolloutDistribution {
  let requested_percentage = clamp_percentage(percentage)
  let mut matched = 0
  let mut min_bucket = 10000
  let mut max_bucket = 0
  for targeting_key in targeting_keys {
    let bucket = rollout_bucket(flag_key, targeting_key)
    if bucket < requested_percentage {
      matched += 1
    }
    if bucket < min_bucket {
      min_bucket = bucket
    }
    if bucket > max_bucket {
      max_bucket = bucket
    }
  }
  let samples = targeting_keys.length()
  {
    samples,
    matched,
    requested_percentage,
    observed_percentage: if samples == 0 {
      0.0
    } else {
      matched.to_double() * 10000.0 / samples.to_double()
    },
    min_bucket: if samples == 0 {
      0
    } else {
      min_bucket
    },
    max_bucket: if samples == 0 {
      0
    } else {
      max_bucket
    },
  }
}

///|
pub fn RolloutDistribution::within_tolerance(
  self : RolloutDistribution,
  tolerance_basis_points : Double,
) -> Bool {
  let requested = self.requested_percentage.to_double()
  (self.observed_percentage - requested).abs() <= tolerance_basis_points
}

///|
pub fn RolloutDistribution::summary(self : RolloutDistribution) -> String {
  self.matched.to_string() +
  "/" +
  self.samples.to_string() +
  " matched; requested=" +
  self.requested_percentage.to_string() +
  ", observed=" +
  self.observed_percentage.to_string()
}