///|
/// A normalized scheme/host/port identity. Paths and queries are not origins.
pub struct HttpOrigin {
  priv value : String
} derive(Eq, Debug)

///|
pub fn HttpUrl::origin(self : HttpUrl) -> HttpOrigin {
  let text = self.to_string()
  let scheme_end = text.find("://").unwrap() + 3
  let authority = text[scheme_end:]
    .split("/")
    .next()
    .unwrap()
    .split("?")
    .next()
    .unwrap()
    .to_owned()
    .to_lower()
  let scheme = text[:scheme_end].to_owned()
  let port_at = if authority.has_prefix("[") {
    authority.find("]:").map(i => i + 1)
  } else {
    authority.find(":")
  }
  let authority = match port_at {
    None => authority
    Some(at) => {
      let mut port = 0
      for digit in authority[at + 1:].iter() {
        port = port * 10 + digit.to_int() - 48
      }
      let host = authority[:at].to_owned()
      if (scheme == "https://" && port == 443) ||
        (scheme == "http://" && port == 80) {
        host
      } else {
        host + ":" + port.to_string()
      }
    }
  }
  { value: scheme + authority, }
}

///|
pub fn HttpOrigin::parse(text : String) -> HttpOrigin? {
  guard HttpUrl::parse(text) is Some(url) else { return None }
  let origin = url.origin()
  let text = url.to_string()
  let authority_start = text.find("://").unwrap() + 3
  let parts = text[authority_start:].split("/").to_array()
  if text.contains("?") ||
    parts.length() > 2 ||
    (parts.length() == 2 && parts[1] != "") {
    return None
  }
  Some(origin)
}

///|
pub fn HttpOrigin::to_string(self : HttpOrigin) -> String {
  self.value
}

///|
pub fn HttpOrigin::permits_credentials(self : HttpOrigin) -> Bool {
  if self.value.has_prefix("https://") {
    return true
  }
  let host = self.value[7:].to_owned()
  host == "localhost" ||
  host.has_prefix("localhost:") ||
  host == "127.0.0.1" ||
  host.has_prefix("127.0.0.1:") ||
  host == "[::1]" ||
  host.has_prefix("[::1]:")
}

///|
/// Secret authorization material. Debug and Show never render the value.
pub struct Authorization {
  priv header : String
} derive(Eq)

///|
pub fn Authorization::bearer(token : String) -> Authorization? {
  if token.is_empty() || token.length() > 4096 {
    return None
  }
  let mut padding = false
  for c in token.iter() {
    if c == '=' {
      padding = true
    } else if padding ||
      !"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._~+/".contains(
        c.to_string(),
      ) {
      return None
    }
  }
  if token.code_unit_at(0) == 61 {
    return None
  }
  Some({ header: "Bearer " + token, })
}

///|
pub fn Authorization::basic(
  username : String,
  password : String,
) -> Authorization? {
  if username.is_empty() ||
    password.is_empty() ||
    username.contains(":") ||
    username.length() + password.length() > 4096 ||
    (username + password).iter().any(c => c.to_int() < 32 || c.to_int() == 127) {
    return None
  }
  Some({
    header: "Basic " +
    @base64.encode(@utf8.encode(username + ":" + password)[:]),
  })
}

///|
/// Only host adapters should call this at the wire boundary. Never log it.
pub fn Authorization::header_value(self : Authorization) -> String {
  self.header
}

///|
pub impl Debug for Authorization with fn to_repr(_) {
  @debug.Repr::literal("")
}

///|
pub impl Show for Authorization with fn output(_, logger) {
  logger.write_string("")
}

///|
/// One explicit authorization per origin. No ambient credentials are discovered.
pub struct Credentials {
  priv entries : Array[(HttpOrigin, Authorization)]
} derive(Eq, Debug)

///|
pub fn Credentials::new(
  entries~ : Array[(HttpOrigin, Authorization)] = [],
) -> Credentials? {
  if entries.length() > 32 {
    return None
  }
  for i, (origin, _) in entries {
    if !origin.permits_credentials() ||
      entries[:i].any(pair => pair.0 == origin) {
      return None
    }
  }
  Some({ entries: entries.copy(), })
}

///|
pub fn Credentials::for_url(
  self : Credentials,
  url : HttpUrl,
) -> Authorization? {
  for (origin, authorization) in self.entries {
    if origin == url.origin() {
      return Some(authorization)
    }
  }
  None
}

///|
pub(all) enum HttpAccept {
  AnyContent
  OctetStream
} derive(Eq, Debug)

///|
/// Authorization is never part of a URL or provenance. Redirects must strip it
/// when the destination's origin differs, even if another credential is configured.
pub(all) struct HttpRequest {
  url : HttpUrl
  authorization : Authorization?
  accept : HttpAccept
} derive(Eq, Debug)

///|
pub fn HttpRequest::anonymous(url : HttpUrl) -> HttpRequest {
  { url, authorization: None, accept: AnyContent, }
}

///|
pub fn HttpRequest::to_string(self : HttpRequest) -> String {
  self.url.to_string()
}

///|
pub fn HttpRequest::location(self : HttpRequest) -> Location {
  self.url.location()
}