///|
/// The hostname scope a user agent would store for a cookie.
pub struct CookieScope {
  domain_ : String
  host_only_ : Bool
} derive(Eq, Debug)

///|
/// Return the canonical ASCII domain stored with the cookie.
pub fn CookieScope::domain(self : CookieScope) -> String {
  self.domain_
}

///|
/// Return whether the cookie is restricted to exactly the request host.
pub fn CookieScope::is_host_only(self : CookieScope) -> Bool {
  self.host_only_
}

///|
/// Test only the domain component of Cookie delivery to a request host.
///
/// A host-only cookie matches exactly one canonical DNS hostname. A Domain
/// cookie also matches descendants at a label boundary. The caller must
/// separately enforce path, Secure, expiration, and other Cookie attributes.
pub fn CookieScope::matches_host(
  self : CookieScope,
  request_host : String,
) -> Result[Bool, CookieScopeError] {
  let host = match canonical_request_host(request_host) {
    Ok(value) => value
    Err(error) => return Err(error)
  }
  Ok(
    host == self.domain_ ||
    (!self.host_only_ && host.has_suffix("." + self.domain_)),
  )
}

///|
/// A request host or Domain attribute that cannot produce a safe cookie scope.
pub(all) enum CookieScopeError {
  InvalidRequestHost(String, String)
  InvalidDomainAttribute(String, String)
  PublicSuffixDomain(String)
  DomainMismatch(String, String)
} derive(Eq, Debug)

///|
pub fn CookieScopeError::message(self : CookieScopeError) -> String {
  match self {
    InvalidRequestHost(host, reason) =>
      "invalid cookie request host '\{host}': \{reason}"
    InvalidDomainAttribute(domain, reason) =>
      "invalid cookie Domain attribute '\{domain}': \{reason}"
    PublicSuffixDomain(domain) =>
      "cookie Domain attribute '\{domain}' is a public suffix"
    DomainMismatch(host, domain) =>
      "request host '\{host}' does not domain-match cookie Domain '\{domain}'"
  }
}

///|
fn canonical_ascii_dns_name(input : String) -> Result[String, String] {
  if input.length() == 0 {
    return Err("domain is empty")
  }
  if input.length() > 253 {
    return Err("domain exceeds 253 ASCII characters")
  }
  let mut label_index = 0
  for label in input.split(".") {
    if label.length() == 0 {
      return Err("domain contains an empty label at index \{label_index}")
    }
    if label.length() > 63 {
      return Err("domain label \{label_index} exceeds 63 ASCII characters")
    }
    if label.has_prefix("-") || label.has_suffix("-") {
      return Err("domain label \{label_index} starts or ends with '-'")
    }
    for character in label {
      if !character.is_ascii() {
        return Err(
          "domain label \{label_index} contains non-ASCII character '\{character}'",
        )
      }
      if !character.is_ascii_alphabetic() &&
        !character.is_ascii_digit() &&
        character != '-' {
        return Err(
          "domain label \{label_index} contains invalid character '\{character}'",
        )
      }
    }
    label_index = label_index + 1
  }
  Ok(input.to_lower())
}

///|
fn canonical_request_host(input : String) -> Result[String, CookieScopeError] {
  let without_root = remove_trailing_root_dot(input)
  match canonical_ascii_dns_name(without_root) {
    Ok(host) => Ok(host)
    Err(reason) => Err(InvalidRequestHost(input, reason))
  }
}

///|
fn canonical_domain_attribute(
  input : String,
) -> Result[String, CookieScopeError] {
  let without_leading_dot = if input.has_prefix(".") {
    input[1:].to_owned()
  } else {
    input
  }
  if without_leading_dot.has_suffix(".") {
    return Err(
      InvalidDomainAttribute(input, "a trailing root dot is not permitted"),
    )
  }
  match canonical_ascii_dns_name(without_leading_dot) {
    Ok(domain) => Ok(domain)
    Err(reason) => Err(InvalidDomainAttribute(input, reason))
  }
}

///|
/// Resolve an optional Cookie Domain attribute using browser-style PSL policy.
///
/// The inputs must be already extracted DNS hostnames. A missing Domain
/// attribute produces a host-only scope. A present attribute may have one
/// compatibility leading dot and must be an ASCII DNS name.
pub fn SuffixList::resolve_cookie_scope(
  self : SuffixList,
  request_host : String,
  domain_attribute : String?,
) -> Result[CookieScope, CookieScopeError] {
  self.resolve_cookie_scope_with_options(
    request_host,
    domain_attribute,
    LookupOptions::browser_default(),
  )
}

///|
/// Resolve a Cookie Domain attribute under an explicit PSL lookup policy.
pub fn SuffixList::resolve_cookie_scope_with_options(
  self : SuffixList,
  request_host : String,
  domain_attribute : String?,
  options : LookupOptions,
) -> Result[CookieScope, CookieScopeError] {
  let host = match canonical_request_host(request_host) {
    Ok(value) => value
    Err(error) => return Err(error)
  }
  match self.lookup_with_options(host, options) {
    Ok(_) => ()
    Err(error) => return Err(InvalidRequestHost(request_host, error.message()))
  }
  let attribute = match domain_attribute {
    None => return Ok({ domain_: host, host_only_: true, })
    Some(value) => value
  }
  let domain = match canonical_domain_attribute(attribute) {
    Ok(value) => value
    Err(error) => return Err(error)
  }
  let domain_lookup = match self.lookup_with_options(domain, options) {
    Ok(value) => value
    Err(error) => return Err(InvalidDomainAttribute(attribute, error.message()))
  }
  if domain_lookup.registrable_domain() is None {
    if domain == host {
      return Ok({ domain_: host, host_only_: true, })
    }
    return Err(PublicSuffixDomain(domain))
  }
  if host != domain && !host.has_suffix("." + domain) {
    return Err(DomainMismatch(host, domain))
  }
  Ok({ domain_: domain, host_only_: false, })
}

///|
/// Enumerate valid, shareable Cookie Domain scopes from narrowest to broadest.
///
/// The request host is included when it is not itself a public suffix. The
/// public suffix is never included. A public-suffix request host therefore
/// yields no shareable scope, even though an equal Domain attribute resolves
/// to a host-only cookie for compatibility.
pub fn SuffixList::shareable_cookie_scopes(
  self : SuffixList,
  request_host : String,
) -> Result[ReadOnlyArray[CookieScope], CookieScopeError] {
  self.shareable_cookie_scopes_with_options(
    request_host,
    LookupOptions::browser_default(),
  )
}

///|
/// Enumerate shareable Cookie Domain scopes under an explicit PSL policy.
pub fn SuffixList::shareable_cookie_scopes_with_options(
  self : SuffixList,
  request_host : String,
  options : LookupOptions,
) -> Result[ReadOnlyArray[CookieScope], CookieScopeError] {
  let host = match canonical_request_host(request_host) {
    Ok(value) => value
    Err(error) => return Err(error)
  }
  let lookup = match self.lookup_with_options(host, options) {
    Ok(value) => value
    Err(error) => return Err(InvalidRequestHost(request_host, error.message()))
  }
  let registrable = match lookup.registrable_domain() {
    Some(value) => value
    None => return Ok(ReadOnlyArray::from_array([]))
  }
  let labels : Array[String] = []
  for label in host.split(".") {
    labels.push(label.to_owned())
  }
  let mut registrable_label_count = 0
  for _ in registrable.split(".") {
    registrable_label_count = registrable_label_count + 1
  }
  let scopes : Array[CookieScope] = []
  for start = 0
      start <= labels.length() - registrable_label_count
      start = start + 1 {
    scopes.push({ domain_: labels[start:].join("."), host_only_: false, })
  }
  Ok(ReadOnlyArray::from_array(scopes))
}