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

///|
/// Where a request goes: scheme, host and port, the unit connections are kept by (RFC 6454).
///
/// An origin is a fact about the URL, not about the connection, so it is settled before either
/// host is asked for one: the browser host has no connection of its own to keep.
priv struct Origin {
  scheme : String
  host : String
  port : Int32
}

///|
fn Origin::of(url : @moonurl.Url) -> Origin {
  let scheme = url.scheme.unwrap_or("http")
  let port = match url.port {
    Some(p) => p
    None => if scheme == "https" { 443 } else { 80 }
  }
  { scheme, host: url.host.unwrap_or(""), port, }
}

///|
fn Origin::key(self : Origin) -> String {
  "\{self.scheme}://\{self.host}:\{self.port}"
}

///|
/// The host and port as a request names them: the port left out when it is the scheme's own.
fn Origin::authority(self : Origin) -> String {
  let host = if self.host.contains(":") { "[\{self.host}]" } else { self.host }
  match (self.scheme, self.port) {
    ("http", 80) | ("https", 443) => host
    _ => "\{host}:\{self.port}"
  }
}

///|
/// The absolute URL a request goes to: joined to `base`, with `query` added to any query the
/// URL already has.
fn located(
  base : @moonurl.Url?,
  text : StringView,
  query : Array[(String, String)]?,
) -> @moonurl.Url raise Refused {
  let given = parse(text)
  let url = match base {
    Some(base) if !given.is_absolute() => {
      let prefix = if base.path.has_suffix("/") {
        base.path
      } else {
        base.path + "/"
      }
      let rest = match given.path.strip_prefix("/") {
        Some(rest) => rest.to_owned()
        None => given.path
      }
      { ..base, path: prefix + rest, query: given.query, fragment: None, }
    }
    _ => given
  }
  check(url, text)
  guard query is Some(pairs) && pairs.length() > 0 else { return url }
  let extra = @moonurl.Query::new(pairs~).encode()
  let joined = match url.query {
    Some(q) if q != "" => q + "&" + extra
    _ => extra
  }
  { ..url, query: Some(joined), }
}

///|
fn parse(text : StringView) -> @moonurl.Url raise Refused {
  @moonurl.Url::decode(text) catch {
    _ => raise Url(text.to_owned())
  }
}

///|
fn check(url : @moonurl.Url, text : StringView) -> Unit raise Refused {
  guard url.scheme is Some("http" | "https") else {
    raise Url("\{text}: not http or https")
  }
  guard url.host is Some(host) && host != "" else {
    raise Url("\{text}: no host")
  }
}

///|
fn has(fields : Array[(String, String)], name : String) -> Bool {
  fields.iter().any(f => f.0.to_lower() == name)
}

///|
fn without(
  fields : Array[(String, String)],
  names : Array[String],
) -> Array[(String, String)] {
  fields.filter(f => !names.contains(f.0.to_lower()))
}

///|
/// The client's headers with the request's laid over them: a name given for the request
/// replaces the client's value for that name.
fn merge(
  base : Array[(String, String)],
  extra : Array[(String, String)],
) -> Array[(String, String)] {
  without(base, extra.map(f => f.0.to_lower())) + extra
}