///|
/// One HTTP field line. Field names are normalized to lowercase ASCII at
/// construction; field values are retained verbatim.
pub(all) struct Header {
  name : String
  value : String
} derive(Debug, Eq)

///|
/// Ordered, duplicate-preserving collection of HTTP fields.
pub(all) struct Headers {
  entries : Array[Header]
} derive(Debug, Eq)

///|
/// Construct a field line. Invalid names are retained so later parsing can emit
/// a structured diagnostic rather than panic.
pub fn header(name : String, value : String) -> Header {
  { name: @lex.lower(name), value, }
}

///|
pub fn Headers::empty() -> Headers {
  { entries: [], }
}

///|
pub fn Headers::from_array(entries : Array[Header]) -> Headers {
  { entries: entries.copy(), }
}

///|
/// Return a new collection with one field appended.
pub fn Headers::add(self : Headers, name : String, value : String) -> Headers {
  let entries = self.entries.copy()
  entries.push(header(name, value))
  { entries, }
}

///|
/// Return all field-line values for a case-insensitive field name.
pub fn Headers::values(self : Headers, name : String) -> Array[String] {
  let normalized = @lex.lower(name)
  let values : Array[String] = []
  for entry in self.entries {
    if entry.name == normalized {
      values.push(entry.value)
    }
  }
  values
}

///|
/// Return the first matching field line.
pub fn Headers::first(self : Headers, name : String) -> String? {
  let normalized = @lex.lower(name)
  for entry in self.entries {
    if entry.name == normalized {
      return Some(entry.value)
    }
  }
  None
}

///|
pub fn Headers::contains(self : Headers, name : String) -> Bool {
  self.first(name) is Some(_)
}

///|
/// Combine repeated list-valued field lines using the RFC 9110 comma form.
pub fn Headers::combined(self : Headers, name : String) -> String? {
  let values = self.values(name)
  if values.length() == 0 {
    return None
  }
  let output = StringBuilder()
  for index, value in values {
    if index > 0 {
      output.write_string(", ")
    }
    output.write_string(value)
  }
  Some(output.to_string())
}

///|
/// Return a copy with every field of this name removed.
pub fn Headers::without(self : Headers, name : String) -> Headers {
  let normalized = @lex.lower(name)
  let entries : Array[Header] = []
  for entry in self.entries {
    if entry.name != normalized {
      entries.push(entry)
    }
  }
  { entries, }
}

///|
/// Return a copy where all old values are replaced by one field line.
pub fn Headers::replace(
  self : Headers,
  name : String,
  value : String,
) -> Headers {
  self.without(name).add(name, value)
}

///|
/// Report invalid field names without rejecting the complete message.
pub fn Headers::diagnostics(self : Headers) -> Array[Diagnostic] {
  let diagnostics : Array[Diagnostic] = []
  for entry in self.entries {
    if !@lex.is_token(entry.name) {
      diagnostics.push({
        level: Error,
        code: "HTTP_FIELD_NAME_INVALID",
        message: "field name is not a valid HTTP token",
        field_name: Some(entry.name),
      })
    }
    if entry.value.contains_char('\u{0000}') ||
      entry.value.contains_char('\r') ||
      entry.value.contains_char('\n') {
      diagnostics.push({
        level: Error,
        code: "HTTP_FIELD_VALUE_UNSAFE",
        message: "field value contains NUL or a line break",
        field_name: Some(entry.name),
      })
    }
  }
  diagnostics
}