///|
fn normalize_host(input : String) -> String raise ScopeError {
  let lowered = input.trim().to_owned().to_lower()
  let host = match lowered.strip_suffix(".") {
    Some(without_dot) => without_dot.to_owned()
    None => lowered
  }
  if host is "" || host.has_prefix(".") || host.has_suffix(".") {
    raise ScopeError::InvalidHost(input)
  }
  for char in host {
    if !(char.is_ascii_alphabetic() ||
      char.is_ascii_digit() ||
      char == '-' ||
      char == '.') {
      raise ScopeError::InvalidHost(input)
    }
  }
  for label_view in host.split(".") {
    let label = label_view.to_owned()
    if label is "" || label.has_prefix("-") || label.has_suffix("-") {
      raise ScopeError::InvalidHost(input)
    }
  }
  host
}

///|
pub fn host_exact(input : String) -> HostScope raise ScopeError {
  { canonical: normalize_host(input), subdomains: false, }
}

///|
pub fn host_and_subdomains(input : String) -> HostScope raise ScopeError {
  { canonical: normalize_host(input), subdomains: true, }
}

///|
pub fn HostScope::canonical(self : HostScope) -> String {
  self.canonical
}

///|
pub fn HostScope::includes_subdomains(self : HostScope) -> Bool {
  self.subdomains
}

///|
pub fn host_contains(grant : HostScope, requested : HostScope) -> Bool {
  if !grant.subdomains {
    return !requested.subdomains && grant.canonical == requested.canonical
  }
  requested.canonical == grant.canonical ||
  requested.canonical.has_suffix("." + grant.canonical)
}

///|
fn normalize_method(input : String) -> String raise ScopeError {
  let http_method = input.trim().to_owned().to_upper()
  if http_method is "" {
    raise ScopeError::InvalidMethod(input)
  }
  for char in http_method {
    if !(char.is_ascii_uppercase() || char == '-') {
      raise ScopeError::InvalidMethod(input)
    }
  }
  http_method
}

///|
pub fn network_scope(
  host : HostScope,
  methods : Array[String],
  max_data_class : DataClass,
) -> NetworkScope raise ScopeError {
  if methods.is_empty() {
    raise ScopeError::EmptyMethodSet
  }
  let normalized : Array[String] = []
  for input in methods {
    let http_method = normalize_method(input)
    if !normalized.contains(http_method) {
      normalized.push(http_method)
    }
  }
  normalized.sort()
  { host, methods: normalized, max_data_class, }
}

///|
fn data_class_rank(classification : DataClass) -> Int {
  match classification {
    Public => 0
    Internal => 1
    Confidential => 2
    Secret => 3
  }
}

///|
fn DataClass::canonical(self : DataClass) -> String {
  match self {
    Public => "public"
    Internal => "internal"
    Confidential => "confidential"
    Secret => "secret"
  }
}

///|
pub fn network_contains(grant : NetworkScope, requested : NetworkScope) -> Bool {
  if !host_contains(grant.host, requested.host) {
    return false
  }
  for http_method in requested.methods {
    if !grant.methods.contains(http_method) {
      return false
    }
  }
  data_class_rank(requested.max_data_class) <=
  data_class_rank(grant.max_data_class)
}

///|
fn NetworkScope::canonical(self : NetworkScope) -> String {
  let host = if self.host.subdomains {
    self.host.canonical + "+subdomains"
  } else {
    self.host.canonical
  }
  host + "[" + self.methods.join(",") + "]<=" + self.max_data_class.canonical()
}