// ordered_headers.mbt — Ordered, multi-value HTTP header model.
//
// A `Map[String, String]` cannot represent HTTP messages faithfully: it loses
// duplicate field names, wire order, the header/trailer distinction, and the
// ability to combine multiple field lines according to the correct RFC 9110
// rule per field. This module provides the ordered model used throughout the
// library.

///|
/// A single HTTP field instance (header or trailer line).
pub(all) struct HeaderField {
  name : String
  value : String
  trailer : Bool
}

///|
/// A wire-ordered list of header and trailer fields.
pub(all) struct OrderedHeaders {
  fields : Array[HeaderField]
}

///|
/// Creates an empty header set.
pub fn OrderedHeaders::new() -> OrderedHeaders {
  { fields: Array::new() }
}

///|
/// Appends a normal header field. `trailer` is set to `false`.
pub fn OrderedHeaders::append(
  self : OrderedHeaders,
  name : String,
  value : String,
) -> Result[Unit, HsError] {
  self.append_with_trailer_flag(name, value, false)
}

///|
/// Appends a trailer field (`trailer = true`).
pub fn OrderedHeaders::append_trailer(
  self : OrderedHeaders,
  name : String,
  value : String,
) -> Result[Unit, HsError] {
  self.append_with_trailer_flag(name, value, true)
}

///|
/// Internal: validates and appends a field with an explicit trailer flag.
fn OrderedHeaders::append_with_trailer_flag(
  self : OrderedHeaders,
  name : String,
  value : String,
  trailer : Bool,
) -> Result[Unit, HsError] {
  try {
    check_header_name(name)
    check_header_value(name, value)
    self.fields.push({ name, value, trailer })
    Ok(())
  } catch {
    e => Err(e)
  }
}

///|
/// Appends a field carrying an explicit trailer flag. Callers with raw wire
/// data use this to preserve the header/trailer distinction.
pub fn OrderedHeaders::append_field(
  self : OrderedHeaders,
  field : HeaderField,
) -> Result[Unit, HsError] {
  self.append_with_trailer_flag(field.name, field.value, field.trailer)
}

///|
/// Replaces every field with the given name (case-insensitive) with a single
/// field carrying `value`, preserving its position and the first instance's
/// trailer flag. If no field matches, `value` is appended as a header.
pub fn OrderedHeaders::set(
  self : OrderedHeaders,
  name : String,
  value : String,
) -> Result[Unit, HsError] {
  try {
    check_header_name(name)
    check_header_value(name, value)
    let mut replaced = false
    let mut i = 0
    while i < self.fields.length() {
      if self.fields[i].name.equal_ignore_ascii_case(name) {
        if replaced {
          let _ = self.fields.remove(i)
        } else {
          let f = self.fields[i]
          self.fields[i] = { name: f.name, value, trailer: f.trailer }
          replaced = true
          i = i + 1
        }
      } else {
        i = i + 1
      }
    }
    if !replaced {
      self.fields.push({ name, value, trailer: false })
    }
    Ok(())
  } catch {
    e => Err(e)
  }
}

///|
/// Removes every field with the given name (case-insensitive).
pub fn OrderedHeaders::remove(self : OrderedHeaders, name : String) -> Unit {
  let mut i = 0
  while i < self.fields.length() {
    if self.fields[i].name.equal_ignore_ascii_case(name) {
      let _ = self.fields.remove(i)
    } else {
      i = i + 1
    }
  }
}

///|
/// Returns the first field value with the given name, or `None` if absent.
pub fn OrderedHeaders::get_first(
  self : OrderedHeaders,
  name : String,
) -> String? {
  for f in self.fields {
    if f.name.equal_ignore_ascii_case(name) {
      return Some(f.value)
    }
  }
  None
}

///|
/// Returns all field values with the given name in wire order.
pub fn OrderedHeaders::get_all(
  self : OrderedHeaders,
  name : String,
) -> Array[String] {
  let out = Array::new()
  for f in self.fields {
    if f.name.equal_ignore_ascii_case(name) {
      out.push(f.value)
    }
  }
  out
}

///|
/// Returns `true` if at least one field with the given name exists.
pub fn OrderedHeaders::contains(self : OrderedHeaders, name : String) -> Bool {
  for f in self.fields {
    if f.name.equal_ignore_ascii_case(name) {
      return true
    }
  }
  false
}

///|
/// Returns all fields in wire order (including trailers).
pub fn OrderedHeaders::entries(self : OrderedHeaders) -> Array[HeaderField] {
  let out = Array::new()
  for f in self.fields {
    out.push({ name: f.name, value: f.value, trailer: f.trailer })
  }
  out
}

///|
/// Returns all non-trailer header values keyed by lower-cased name, in wire
/// order, with duplicates preserved.
pub fn OrderedHeaders::header_values(
  self : OrderedHeaders,
) -> Array[(String, String)] {
  let out = Array::new()
  for f in self.fields {
    if !f.trailer {
      out.push((f.name.to_lower(), f.value))
    }
  }
  out
}

///|
/// Returns all trailer values keyed by lower-cased name, in wire order.
pub fn OrderedHeaders::trailer_values(
  self : OrderedHeaders,
) -> Array[(String, String)] {
  let out = Array::new()
  for f in self.fields {
    if f.trailer {
      out.push((f.name.to_lower(), f.value))
    }
  }
  out
}

///|
/// Combines multiple field lines for a single name per RFC 9110 §5.2.
///
/// For most fields, RFC 9110 defines a "combined value" by joining the
/// field values with `", "` — but only when the field is a list-based field
/// whose grammar permits it. Some fields (notably `set-cookie`) must never be
/// combined. This function applies the safe default `", "` and rejects
/// `set-cookie`, which requires an explicit per-field policy.
pub fn OrderedHeaders::combined_value(
  self : OrderedHeaders,
  name : String,
) -> Result[String, HsError] {
  let all = self.get_all(name)
  if all.is_empty() {
    return Err(
      hs_error(MessageConstruction, MissingComponent, "no field named " + name),
    )
  }
  if name.equal_ignore_ascii_case("set-cookie") {
    return Err(
      hs_error(
        MessageConstruction,
        InvalidComponentCombination,
        "set-cookie must not be combined; use a per-field policy",
      ),
    )
  }
  if all.length() == 1 {
    return Ok(all[0])
  }
  let mut out = all[0]
  for i = 1; i < all.length(); i = i + 1 {
    out = out + ", " + all[i]
  }
  Ok(out)
}

///|
/// Combines multiple field lines with the caller-supplied joiner. This is the
/// escape hatch for fields (like `set-cookie`) whose combination rule is not
/// the RFC 9110 default.
pub fn OrderedHeaders::combined_value_with(
  self : OrderedHeaders,
  name : String,
  joiner : String,
) -> Result[String, HsError] {
  let all = self.get_all(name)
  if all.is_empty() {
    return Err(
      hs_error(MessageConstruction, MissingComponent, "no field named " + name),
    )
  }
  if all.length() == 1 {
    return Ok(all[0])
  }
  let mut out = all[0]
  for i = 1; i < all.length(); i = i + 1 {
    out = out + joiner + all[i]
  }
  Ok(out)
}

///|
/// Returns the number of stored fields.
pub fn OrderedHeaders::length(self : OrderedHeaders) -> Int {
  self.fields.length()
}