///|
pub(all) struct Headers {
  entries : Array[(String, String)]
  status : Int32?
  description : String
} derive(Eq, Debug)

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

///|
pub extend Headers with @debug.Debug::{to_repr}

///|
pub fn Headers::new(entries~ : Array[(String, String)] = []) -> Headers {
  { entries: entries.copy(), status: None, description: "", }
}

///|
pub fn Headers::get_all(self : Headers, name : String) -> Array[String] {
  self.entries
  .iter()
  .filter_map(pair => if pair.0 == name { Some(pair.1) } else { None })
  .to_array()
}

///|
pub fn Headers::get(self : Headers, name : String) -> String? {
  for (key, value) in self.entries {
    if key == name {
      return Some(value)
    }
  }
  None
}

///|
pub fn Headers::encode(self : Headers) -> Bytes raise ProtocolError {
  let out = StringBuilder()
  out.write_string("NATS/1.0")
  if self.status is Some(code) {
    guard code >= 100 && code <= 999 else {
      raise Invalid("invalid header status")
    }
    guard !self.description.contains("\r") && !self.description.contains("\n") else {
      raise Invalid("invalid status description")
    }
    out.write_string(" \{code}")
    if !self.description.is_empty() {
      out.write_string(" " + self.description)
    }
  }
  out.write_string("\r\n")
  for (name, value) in self.entries {
    guard token(name) &&
      !name.contains(":") &&
      !value.contains("\r") &&
      !value.contains("\n") &&
      !value.contains("\u0000") else {
      raise Invalid("invalid header field")
    }
    out.write_string("\{name}: \{value}\r\n")
  }
  out.write_string("\r\n")
  @utf8.encode(out.to_string())
}

///|
pub fn Headers::parse(raw : Bytes) -> Headers raise ProtocolError {
  let text = @utf8.decode(raw) catch {
    _ => raise Invalid("invalid header UTF-8")
  }
  guard text.has_suffix("\r\n\r\n") else { raise Invalid("header terminator") }
  let lines = text.split("\r\n").map(s => s.to_owned()).to_array()
  guard lines[0] == "NATS/1.0" || lines[0].has_prefix("NATS/1.0 ") else {
    raise Invalid("header version")
  }
  let mut status = None
  let mut description = ""
  if lines[0].length() > 8 {
    let parts = lines[0][9:].split(" ").map(s => s.to_owned()).to_array()
    let code = number(parts[0])
    guard parts[0].length() == 3 && code >= 100 else {
      raise Invalid("invalid status")
    }
    status = Some(code)
    description = parts[1:].iter().join(" ")
  }
  let entries = []
  for i in 1..<(lines.length() - 2) {
    let line = lines[i]
    let colon = line.find(":")
    guard colon is Some(at) && at > 0 else {
      raise Invalid("invalid header line")
    }
    let name = line[:at].to_owned()
    let value = line[at + 1:].trim().to_owned()
    guard token(name) &&
      !value.contains("\r") &&
      !value.contains("\n") &&
      !value.contains("\u0000") else {
      raise Invalid("invalid header field")
    }
    entries.push((name, value))
  }
  { entries, status, description, }
}

///|
pub fn publish_headers(
  subject_name : String,
  headers : Headers,
  payload : Bytes,
  reply_to? : String,
) -> Bytes raise ProtocolError {
  guard subject(subject_name, false) else {
    raise Invalid("invalid publish subject")
  }
  let reply = match reply_to {
    Some(r) => {
      guard subject(r, false) else { raise Invalid("invalid reply subject") }
      " " + r
    }
    None => ""
  }
  let h = headers.encode()
  @utf8.encode(
    "HPUB \{subject_name}\{reply} \{h.length()} \{h.length() + payload.length()}\r\n",
  ) +
  h +
  payload +
  b"\r\n"
}