///|
fn has_tag(tags : Array[String], tag : String) -> Bool {
  for item in tags {
    if lower(item) == lower(tag) {
      return true
    }
  }
  false
}

///|
fn has_all_tags(tags : Array[String], required : Array[String]) -> Bool {
  for tag in required {
    if !has_tag(tags, tag) {
      return false
    }
  }
  true
}

///|
fn has_any_excluded_tag(tags : Array[String], excluded : Array[String]) -> Bool {
  for tag in excluded {
    if has_tag(tags, tag) {
      return true
    }
  }
  false
}

///|
fn region_in(regions : Array[Region], region : Region) -> Bool {
  for item in regions {
    if item == region {
      return true
    }
  }
  false
}

///|
fn tier_in(tiers : Array[UserTier], tier : UserTier) -> Bool {
  for item in tiers {
    if item == tier {
      return true
    }
  }
  false
}

///|
pub fn segment_matches(
  user : UserProfile,
  rule : SegmentRule,
  salt? : String = "",
) -> Bool {
  if rule.include_regions.length() > 0 &&
    !region_in(rule.include_regions, user.region) {
    return false
  }
  if region_in(rule.exclude_regions, user.region) {
    return false
  }
  if rule.include_tiers.length() > 0 && !tier_in(rule.include_tiers, user.tier) {
    return false
  }
  if tier_in(rule.exclude_tiers, user.tier) {
    return false
  }
  if !has_all_tags(user.tags, rule.require_tags) {
    return false
  }
  if has_any_excluded_tag(user.tags, rule.exclude_tags) {
    return false
  }
  if has_text(rule.min_version) &&
    version_score(user.version) < version_score(rule.min_version) {
    return false
  }
  if rule.max_bucket_bp < 10000 {
    return rollout_bucket(user.id, salt) < clamp(rule.max_bucket_bp, 0, 10000)
  }
  true
}

///|
pub fn matches_any_segment(
  user : UserProfile,
  segments : Array[SegmentRule],
  salt? : String = "",
) -> Bool {
  if segments.length() == 0 {
    return true
  }
  for segment in segments {
    if segment_matches(user, segment, salt~) {
      return true
    }
  }
  false
}

///|
pub fn matching_segment_names(
  user : UserProfile,
  segments : Array[SegmentRule],
  salt? : String = "",
) -> Array[String] {
  let names = Array::new(capacity=segments.length())
  for segment in segments {
    if segment_matches(user, segment, salt~) {
      names.push(segment.name)
    }
  }
  names
}

///|
pub fn segment_summary(rule : SegmentRule) -> String {
  let out = StringBuilder()
  out.write_string(rule.name)
  out.write_string(" regions=" + rule.include_regions.length().to_string())
  out.write_string(" tiers=" + rule.include_tiers.length().to_string())
  out.write_string(" required_tags=" + rule.require_tags.length().to_string())
  out.write_string(" excluded_tags=" + rule.exclude_tags.length().to_string())
  if has_text(rule.min_version) {
    out.write_string(" min_version=" + rule.min_version)
  }
  if rule.max_bucket_bp < 10000 {
    out.write_string(" pre_bucket=" + percent_text(rule.max_bucket_bp))
  }
  out.to_string()
}

///|
pub fn count_segment_matches(
  users : Array[UserProfile],
  rule : SegmentRule,
  salt? : String = "",
) -> Int {
  let mut total = 0
  for user in users {
    if segment_matches(user, rule, salt~) {
      total += 1
    }
  }
  total
}