///|
/// Which HTTP proxies a `Client` uses, as data fixed at construction: one
/// URL for `http://` requests, one for `https://` requests, and a
/// `NO_PROXY` list of the hosts that go direct. Build it with `none`,
/// `fixed` or `from_env`, or as a record.
///
/// A proxy URL is `http://` or `https://` (a missing scheme is `http://`),
/// with an optional `user:password@` that becomes `Proxy-Authorization:
/// Basic` for that proxy alone. `http://` requests are forwarded with an
/// absolute target; `https://` requests go through a `CONNECT` tunnel, with
/// TLS end to end inside it. A setting that cannot be used fails the
/// requests it would carry with `Protocol` in phase Pool, rather than
/// sending them direct.
///
/// ```mbt check
/// test {
///   let env = {
///     "https_proxy": "http://proxy.test:3128",
///     "NO_PROXY": "internal.test",
///   }
///   let proxy = @h1client.ProxyConfig::from_env(name => env.get(name))
///   assert_eq(proxy.http, None)
///   assert_eq(proxy.https, Some("http://proxy.test:3128"))
///   assert_eq(proxy.no_proxy, "internal.test")
/// }
/// ```
#derive(Eq, Debug)
pub(all) struct ProxyConfig {
  /// The proxy for `http://` requests, or `None` to send them direct.
  http : String?
  /// The proxy for `https://` requests, or `None` to send them direct.
  https : String?
  /// Hosts that skip the proxies, as curl reads `NO_PROXY`: comma-separated
  /// names (each with its subdomains), `*`, IPv4 and IPv6 addresses and
  /// `/prefix` networks, each optionally restricted to a `:port`. Names are
  /// never resolved; entries of no known form are skipped.
  no_proxy : String
}

///|
/// Compares proxy settings.
pub extend ProxyConfig with Eq::{equal, not_equal}

///|
/// Formats proxy settings for diagnostics; they may hold credentials.
pub extend ProxyConfig with @debug.Debug::{to_repr}

///|
/// Every request goes direct.
pub fn ProxyConfig::none() -> ProxyConfig {
  { http: None, https: None, no_proxy: "", }
}

///|
/// Every request goes through the proxy at `url`, as httpx's `proxy=`.
pub fn ProxyConfig::fixed(url : String) -> ProxyConfig {
  { http: Some(url), https: Some(url), no_proxy: "", }
}

///|
/// The proxies the environment names, read once through `lookup`, as curl
/// and httpx read them:
///
/// - `http_proxy` for `http://` requests, `https_proxy` for `https://`, and
///   `all_proxy` for either when its own is unset;
/// - `no_proxy` for the hosts that go direct;
/// - each name in lower case first, then upper case; a value of only
///   spaces is unset. Upper-case `HTTP_PROXY` is ignored when
///   `REQUEST_METHOD` is set, since a CGI program gets it from a request's
///   `Proxy` header (httpoxy, CVE-2016-5385), as Go and Python ignore it.
///
/// The client never reads the environment itself; `lookup` is called here
/// and nowhere else.
pub fn ProxyConfig::from_env(lookup : (String) -> String?) -> ProxyConfig {
  let cgi = lookup("REQUEST_METHOD") is Some(value) && !value.is_empty()
  let all = env_value(lookup, "all_proxy", upper=true)
  let own = (value : String?) => if value is Some(_) { value } else { all }
  {
    http: own(env_value(lookup, "http_proxy", upper=!cgi)),
    https: own(env_value(lookup, "https_proxy", upper=true)),
    no_proxy: env_value(lookup, "no_proxy", upper=true).unwrap_or(""),
  }
}

///|
/// The first value of `name`, then of its upper-case form when `upper`,
/// that is not blank, trimmed.
fn env_value(
  lookup : (String) -> String?,
  name : String,
  upper~ : Bool,
) -> String? {
  let names = if upper { [name, name.to_upper()] } else { [name] }
  for candidate in names {
    if lookup(candidate) is Some(value) && !value.trim().is_empty() {
      return Some(value.trim().to_owned())
    }
  }
  None
}

///|
/// A proxy server: where to dial, and what to tell it.
priv struct ProxyServer {
  host : String
  port : Int32
  secure : Bool
  /// `http://host:port` or `https://host:port`, the port always written.
  origin : String
  /// `Basic` credentials from the proxy URL's userinfo; they go to the
  /// proxy alone.
  authorization : String?
}

///|
/// How a request reaches its origin.
priv enum Route {
  Direct
  /// An `http://` request handed to the proxy with its target in absolute
  /// form (RFC 9112 §3.2.2).
  Forward(ProxyServer)
  /// An `https://` request sent through a tunnel the proxy opens with
  /// `CONNECT` (RFC 9110 §9.3.6), TLS running end to end inside it.
  Tunnel(ProxyServer)
}

///|
/// A `ProxyConfig` parsed once: each scheme's proxy or why it cannot be
/// used, and the bypass rules.
priv struct Proxies {
  http : Result[ProxyServer, String]?
  https : Result[ProxyServer, String]?
  bypass : Array[NoProxyRule]
}

///|
fn Proxies::new(config : ProxyConfig) -> Proxies {
  {
    http: config.http.map(url => parse_proxy(url, scheme="http")),
    https: config.https.map(url => parse_proxy(url, scheme="https")),
    bypass: parse_no_proxy(config.no_proxy),
  }
}

///|
/// The proxy at `text`, set for requests of `scheme`: `http://` or
/// `https://`, a missing scheme taken as `http://` (curl, httpx), a missing
/// port as the scheme's default, and `user:password@` percent-decoded into
/// `Basic` credentials. A path is ignored. Error messages never repeat the
/// URL, which may hold credentials.
fn parse_proxy(text : String, scheme~ : String) -> Result[ProxyServer, String] {
  let source = "\{scheme} proxy"
  let text = text.trim().to_owned()
  let text = if text.contains("://") { text } else { "http://" + text }
  let url = @mttp_core.Url::parse(text) catch {
    @mttp_core.UrlError::Invalid(reason) =>
      return Err("invalid \{source} URL: \{reason}")
  }
  let proxy_scheme = url.scheme.unwrap_or("")
  guard proxy_scheme is ("http" | "https") else {
    return Err(
      "unsupported \{source} scheme \{proxy_scheme}: only http and https proxies are supported",
    )
  }
  guard url.host is Some(host) && !host.is_empty() else {
    return Err("invalid \{source} URL: no host")
  }
  let authorization = match url.userinfo {
    None => None
    Some(userinfo) =>
      match basic_credentials(userinfo) {
        Some(credentials) => Some(credentials)
        None => return Err("invalid \{source} URL: malformed credentials")
      }
  }
  let secure = proxy_scheme == "https"
  let port = url.port_or_default().unwrap_or(if secure { 443 } else { 80 })
  let name = if host.contains(":") { "[" + host + "]" } else { host }
  Ok({
    host,
    port,
    secure,
    origin: "\{proxy_scheme}://\{name}:\{port}",
    authorization,
  })
}

///|
/// `Basic` credentials (RFC 7617) for a URL's userinfo, its user and
/// password percent-decoded; a userinfo without `:` is a user with an empty
/// password, as httpx takes it.
fn basic_credentials(userinfo : String) -> String? {
  let (user, password) = match userinfo.find(":") {
    Some(colon) => (userinfo[:colon], userinfo[colon + 1:])
    None => (userinfo[:], "")
  }
  let user = @mttp_core.percent_decode(user.to_owned()) catch {
    _ => return None
  }
  let password = @mttp_core.percent_decode(password.to_owned()) catch {
    _ => return None
  }
  @mttp_core.Request::get("/").basic(user, password).headers.get(
    "authorization",
  )
}

///|
/// The route of a request to `target`: direct when its scheme has no proxy
/// or the bypass rules name its host, otherwise forwarded (`http://`) or
/// tunnelled (`https://`). Each request is routed by its own URL. An
/// unusable proxy setting raises `Protocol` in phase Pool, before anything
/// is sent.
fn Proxies::route(
  self : Proxies,
  target : Target,
) -> Route raise @mttp_core.TransportError {
  guard (if target.secure { self.https } else { self.http }) is Some(setting) else {
    return Direct
  }
  if bypasses_proxy(self.bypass, target.host, target.port) {
    return Direct
  }
  match setting {
    Ok(server) => if target.secure { Tunnel(server) } else { Forward(server) }
    Err(message) =>
      raise @mttp_core.TransportError(kind=Protocol, phase=Pool, detail=message)
  }
}

///|
/// The key that parks and counts connections for `origin` on this route:
/// a connection to a proxy or through its tunnel is never handed to a
/// request that goes direct, or through another proxy.
fn Route::key(self : Route, origin : String) -> String {
  match self {
    Direct => origin
    Forward(server) | Tunnel(server) => origin + " via " + server.origin
  }
}

///|
/// The request target: origin form directly and through a tunnel, absolute
/// form to a forward proxy; a CONNECT keeps its authority on every route.
fn Route::target(
  self : Route,
  target : Target,
  meth : @mttp_core.Method,
) -> String {
  match self {
    Forward(_) if meth != Connect => "http://" + target.host_field + target.path
    _ => target.path
  }
}

///|
/// The headers as this route sends them: the request's own
/// `Proxy-Authorization` is always dropped, and a forward proxy's
/// credentials are added; a tunnel's go on its CONNECT instead.
fn Route::headers(
  self : Route,
  headers : @mttp_core.Headers,
) -> @mttp_core.Headers {
  let headers = headers.remove("proxy-authorization")
  match self {
    Forward({ authorization: Some(auth), .. }) =>
      headers.add("proxy-authorization", auth)
    _ => headers
  }
}

///|
/// Opens a connection for `route`: to the origin, or to the proxy, through
/// whose tunnel TLS then runs to the origin. A connection carries at most
/// two TLS layers, the proxy's and the origin's.
async fn dial_route(
  dialer : &@rt.Dialer,
  target : Target,
  route : Route,
  agent : String?,
  limits : @h1.Limits,
) -> Wire raise @mttp_core.TransportError {
  match route {
    Direct => Wire::dial(dialer, target.host, target.port, target.secure)
    Forward(proxy) => Wire::dial(dialer, proxy.host, proxy.port, proxy.secure)
    Tunnel(proxy) => {
      let wire = Wire::dial(dialer, proxy.host, proxy.port, proxy.secure)
      errdefer wire.close()
      open_tunnel(wire, target, proxy, agent, limits)
      wire.conn = dialer.start_tls(wire.conn, host=target.host)
      wire
    }
  }
}

///|
/// Asks the proxy for a tunnel to `target` and reads its answer within
/// `limits`, all in phase Dial: a refusal (3xx and above) is
/// `Proxy(status)`; a malformed answer, 101, more than 16 informational
/// heads, or bytes after a successful answer are `Protocol`.
async fn open_tunnel(
  wire : Wire,
  target : Target,
  proxy : ProxyServer,
  agent : String?,
  limits : @h1.Limits,
) -> Unit raise @mttp_core.TransportError {
  let mut headers = @mttp_core.Headers::of([("host", target.authority)])
  if proxy.authorization is Some(auth) {
    headers = headers.add("proxy-authorization", auth)
  }
  if agent is Some(agent) {
    headers = headers.add("user-agent", agent)
  }
  let head = @h1.write_request_head(
    Connect,
    target.authority,
    headers,
    NoBody,
    Http11,
  ) catch {
    @h1.ParseError(detail~, ..) => raise invalid(detail)
  }
  wire.write(head)
  for _ in 0..<17 {
    let answer = wire.head(limits, phase=Dial)
    if answer.status >= 300 {
      raise @mttp_core.TransportError(
        kind=Proxy(status=answer.status),
        phase=Dial,
        detail="the proxy refused CONNECT",
      )
    }
    if answer.status >= 200 {
      if !wire.pending.is_empty() {
        raise @mttp_core.TransportError(
          kind=Protocol,
          phase=Dial,
          detail="unexpected bytes after the proxy's CONNECT answer",
        )
      }
      return
    }
    if answer.status == 101 {
      break
    }
  }
  raise @mttp_core.TransportError(
    kind=Protocol,
    phase=Dial,
    detail="invalid informational answer to CONNECT",
  )
}