///|
/// The client and scheme a reverse proxy saw, put back in the scope (← uvicorn's
/// `ProxyHeadersMiddleware`, and RFC 7239). Only a request from an address in
/// `trusted` is rewritten, since anyone else could claim any address; `"*"`
/// trusts every peer.
///
/// `X-Forwarded-For` gives the client: the rightmost address that is not
/// trusted, every proxy before it having appended its own; when all are
/// trusted, the leftmost. `X-Forwarded-Proto` gives the scheme, `http` or
/// `https` (`ws` or `wss` for a WebSocket). Without either header, the
/// `Forwarded` header is read the same way through its `for` and `proto`.
pub(all) struct ProxyHeaders {
  trusted : Array[String]
} derive(Eq, Debug)

///|
pub extend ProxyHeaders with Eq::{equal, not_equal}

///|
pub extend ProxyHeaders with Debug::{to_repr}

///|
/// uvicorn's default: only a proxy on the same machine is believed.
pub let proxy_headers : ProxyHeaders = { trusted: ["127.0.0.1"], }

///|
pub fn ProxyHeaders::new(
  trusted~ : Array[String] = proxy_headers.trusted,
) -> ProxyHeaders {
  { trusted, }
}

///|
fn ProxyHeaders::trusts(self : ProxyHeaders, host : String) -> Bool {
  self.trusted.contains("*") || self.trusted.contains(host)
}

///|
/// The client and the scheme the headers name, each `None` when they name none.
fn ProxyHeaders::forwarded(
  self : ProxyHeaders,
  headers : Array[(String, String)],
) -> (String?, String?) {
  let fors = list(headers, "x-forwarded-for")
  let protos = list(headers, "x-forwarded-proto")
  if !fors.is_empty() || !protos.is_empty() {
    return (self.pick(fors).map(p => p.0), protos.get(0))
  }
  // RFC 7239: each element is one hop, `for=…;proto=…`, the client's first.
  let hops = list(headers, "forwarded").map(element => {
    let fields : Map[String, String] = Map([])
    for pair in element.split(";") {
      if pair.find("=") is Some(k) {
        let key = pair.view(end_offset=k).trim(chars=" ").to_lower()
        let value = pair.view(start_offset=k + 1).trim(chars=" \"").to_owned()
        fields[key.to_owned()] = value
      }
    }
    (address(fields.get("for").unwrap_or("")), fields.get("proto"))
  })
  match self.pick(hops.map(h => h.0)) {
    Some((_, i)) => (Some(hops[i].0), hops[i].1)
    None => (None, None)
  }
}

///|
/// The client among `chain` and its position: the rightmost untrusted entry,
/// or the leftmost when every entry is trusted.
fn ProxyHeaders::pick(
  self : ProxyHeaders,
  chain : Array[String],
) -> (String, Int32)? {
  for i = chain.length() - 1; i >= 0; i = i - 1 {
    if !self.trusts(chain[i]) {
      return Some((chain[i], i))
    }
  }
  if chain.is_empty() {
    None
  } else {
    Some((chain[0], 0))
  }
}

///|
/// Every comma-separated entry of every header named `name`, trimmed.
fn list(headers : Array[(String, String)], name : String) -> Array[String] {
  let out = []
  for h in headers {
    if h.0.to_lower() == name {
      for part in h.1.split(",") {
        let part = part.trim(chars=" ").to_owned()
        if !part.is_empty() {
          out.push(part)
        }
      }
    }
  }
  out
}

///|
/// A `Forwarded` node without its port and IPv6 brackets.
fn address(node : String) -> String {
  if node.has_prefix("[") {
    match node.find("]") {
      Some(end) => node.view(start_offset=1, end_offset=end).to_owned()
      None => node
    }
  } else {
    match node.find(":") {
      Some(k) if node.rev_find(":") == Some(k) =>
        node.view(end_offset=k).to_owned()
      _ => node
    }
  }
}

///|
fn scheme_for(proto : String?, websocket : Bool) -> String? {
  match (proto.map(p => p.to_lower()), websocket) {
    (Some("https" | "wss"), true) => Some("wss")
    (Some("http" | "ws"), true) => Some("ws")
    (Some("https" | "wss"), false) => Some("https")
    (Some("http" | "ws"), false) => Some("http")
    _ => None
  }
}

///|
pub impl Layer for ProxyHeaders with fn wrap(self, app) {
  (scope, receive, send) => {
    let scope = match scope {
      Http(hs) if hs.client is Some((peer, _)) && self.trusts(peer) => {
        let (client, proto) = self.forwarded(hs.headers)
        @spec.Scope::Http({
          ..hs,
          client: if client is Some(c) {
            Some((c, 0))
          } else {
            hs.client
          },
          scheme: scheme_for(proto, false).unwrap_or(hs.scheme),
        })
      }
      WebSocket(ws) if ws.client is Some((peer, _)) && self.trusts(peer) => {
        let (client, proto) = self.forwarded(ws.headers)
        @spec.Scope::WebSocket({
          ..ws,
          client: if client is Some(c) {
            Some((c, 0))
          } else {
            ws.client
          },
          scheme: scheme_for(proto, true).unwrap_or(ws.scheme),
        })
      }
      other => other
    }
    app(scope, receive, send)
  }
}

///|
pub extend ProxyHeaders with Layer::{wrap}