///|
/// A `field-name` is any printable ASCII character except `:` (RFC 5322 3.6.1,
/// `ftext`). MoonMail additionally requires a nonempty name.
pub fn is_valid_field_name(name : String) -> Bool {
  if name.is_empty() {
    return false
  }
  for ch in name {
    let c = ch.to_int()
    if !((c >= 33 && c <= 57) || (c >= 59 && c <= 126)) {
      return false
    }
  }
  true
}

///|
/// Check a header value for injection: CR, LF and NUL are all rejected
/// (RFC 5322 3.6.8 / security model). See `injection.mbt`.
pub fn check_header_value(
  value : String,
  context : String,
) -> Unit raise MailFailure {
  reject_crlf(value, context)
}

///|
/// Build a single (possibly folded) header field `Name: value` followed by
/// CRLF. The value is validated against CR/LF injection and folded so that no
/// line exceeds `max_width` (default 78) columns, per RFC 5322 2.2.3.
pub fn make_header(
  name : String,
  value : String,
  max_width? : Int = 78,
) -> String raise MailFailure {
  guard is_valid_field_name(name) else {
    raise MailFailure::of(
      InvalidHeader,
      MM_HEADER_004,
      "invalid header field name: \{name}",
    )
  }
  reject_crlf(value, "header '\{name}'")
  fold_header_value(name, value, max_width)
}

///|
/// Fold `value` after the `Name: ` prefix so every line is <= `max_width`.
/// Continuation lines start with a single space (RFC 5322 folding whitespace).
fn fold_header_value(name : String, value : String, max_width : Int) -> String {
  let prefix = "\{name}: "
  let text = prefix + value
  let bytes = string_to_bytes(text)
  let out = FixedArray::make(bytes.length() + 32, b'\x00')
  let mut j = 0
  let mut i = 0
  let mut col = 0
  while i < bytes.length() {
    let remaining = bytes.length() - i
    if col + remaining <= max_width {
      for k = i; k < bytes.length(); k = k + 1 {
        out[j] = bytes[k]
        j += 1
      }
      break
    }
    let window = max_width - col
    if window < 1 {
      break
    }
    let last_space = find_last_space(bytes, i, i + window)
    if last_space > i {
      for k = i; k < last_space; k = k + 1 {
        out[j] = bytes[k]
        j += 1
      }
      out[j] = b'\r'
      out[j + 1] = b'\n'
      out[j + 2] = b' '
      j += 3
      i = last_space + 1
      col = 1
    } else {
      // no whitespace to break at: hard break inside the token
      for k = i; k < i + window; k = k + 1 {
        out[j] = bytes[k]
        j += 1
      }
      out[j] = b'\r'
      out[j + 1] = b'\n'
      out[j + 2] = b' '
      j += 3
      i = i + window
      col = 1
    }
  }
  fixedarray_to_string(out, j) + "\r\n"
}

///|
/// Find the index of the last space within `bytes[start:end]`, or `-1`.
fn find_last_space(bytes : Bytes, start : Int, end : Int) -> Int {
  let mut idx = -1
  for j = start; j < end; j = j + 1 {
    if bytes[j] == b' ' {
      idx = j
    }
  }
  idx
}

///|
/// Remove folding whitespace: each CRLF followed by WSP is removed and the
/// WSP collapsed. Used by tests and by MIME parsers.
pub fn unfold_header_value(value : String) -> String {
  let bytes = string_to_bytes(value)
  let out = FixedArray::make(bytes.length(), b'\x00')
  let mut j = 0
  let mut i = 0
  while i < bytes.length() {
    if bytes[i] == b'\r' && i + 1 < bytes.length() && bytes[i + 1] == b'\n' {
      let mut k = i + 2
      while k < bytes.length() && (bytes[k] == b' ' || bytes[k] == b'\t') {
        k += 1
      }
      if k > i + 2 {
        // collapsed folding whitespace: emit one space
        out[j] = b' '
        j += 1
      }
      i = k
    } else {
      out[j] = bytes[i]
      j += 1
      i += 1
    }
  }
  fixedarray_to_string(out, j)
}