// Copyright 2026 Leo Cheng
// SPDX-License-Identifier: Apache-2.0

///|
/// The names this library is used by, in one import: the connection, the datagram socket and
/// the contract. A caller that wants an adapter names that package, so a program that only
/// opens TCP links nothing else.
pub using @tcp {type Conn, type Listener, listen, serve, dial}

///|
pub using @udp {type Socket, bind}

///|
pub using @spec {
  type Body,
  type Field,
  type Refused,
  type Request,
  type Response,
  type Stream,
}

///|
/// What `connect` settled on.
pub(all) struct Opened {
  /// The connection, ready to carry an exchange.
  conn : @tcp.Conn
  /// The protocol ALPN settled on, or `""` in the clear.
  alpn : String
  /// For `http` and `https`: the path and query the URL named, which an HTTP request needs.
  target : String
}

///|
/// Connect to `url` in one line: the scheme decides TLS, the ALPN list is offered, and the
/// chain is judged against the roots the caller passes — Mozilla's when none are given.
///
/// The connection is returned before any protocol is chosen for it: `alpn` says what the
/// server picked, and the matching adapter is the one to hand it to.
///
/// ```moonbit nocheck
/// let opened = @moonnet.connect("https://example.com/", alpn=["h2", "http/1.1"])
/// if opened.alpn == "h2" {
///   let conn = @h2.client(opened.conn, scheme="https", authority="example.com")
///   let (response, body) = @spec.call(conn, @spec.Request::new("GET", opened.target))
///   println("\{response.status} \{body.length()}")
/// }
/// ```
pub async fn connect(
  url : StringView,
  alpn~ : Array[String] = ["h2", "http/1.1"],
  roots? : @x509.Roots,
  now? : Int64,
) -> Opened {
  let parsed = @url.Url::decode(url) catch {
    _ => raise @spec.Refused::Unsupported("a URL this cannot read")
  }
  let secure = match parsed.scheme {
    Some("https") | Some("wss") => true
    Some("http") | Some("ws") => false
    Some(other) =>
      raise @spec.Refused::Unsupported("a scheme this cannot open: \{other}")
    None => raise @spec.Refused::Unsupported("a URL with no scheme")
  }
  guard parsed.host is Some(host) else {
    raise @spec.Refused::Unsupported("a URL with no host")
  }
  let port = parsed.port.unwrap_or(if secure { 443 } else { 80 })
  let target = match parsed.query {
    Some(query) => "\{parsed.path}?\{query}"
    None => parsed.path
  }
  // Forwarded as an option, since `dial` asks for the config rather than for an option of one.
  let tls : @tls.Client? = if secure {
    Some(@tls.Client::new(roots.unwrap_or(@roots.mozilla()), alpn~))
  } else {
    None
  }
  let conn = @tcp.dial(host, port~, tls?, now?)
  { conn, alpn: conn.alpn().unwrap_or(""), target, }
}