///|
#derive(Eq, Debug)
pub(all) enum MimeMode {
  Strict
  Lenient
}

///|
#derive(Eq, Debug)
pub(all) suberror ParseError {
  Malformed(String)
  Unsupported(String)
  CoreValidation(@core.ValidationError)
}

///|
pub(open) trait MimeWriter {
  async fn write_text(Self, String) -> Unit
}

///|
priv struct CollectWriter {
  builder : StringBuilder
}

///|
fn CollectWriter::new() -> CollectWriter {
  { builder: StringBuilder(), }
}

///|
impl MimeWriter for CollectWriter with fn write_text(self, text) {
  self.builder.write_string(text)
}

///|
/// Minimal synchronous serializer kept for in-memory use cases and tests.
/// File-backed attachment sources require `render_to_rfc5322` or `write_to`.
pub fn to_rfc5322(message : @core.Message) -> String {
  if contains_file_source(message.body) {
    abort("file-backed attachments require async render_to_rfc5322/write_to")
  }
  let builder = StringBuilder()
  write_message_headers_sync(builder, message, Strict)
  builder.write_string("\r\n")
  write_body_sync(builder, message.body, [], Strict)
  builder.to_string()
}

///|
pub async fn render_to_rfc5322(
  message : @core.Message,
  mode~ : MimeMode = Strict,
) -> String {
  let writer = CollectWriter::new()
  write_to(writer, message, mode~)
  writer.builder.to_string()
}

///|
pub async fn[W : MimeWriter] write_to(
  writer : W,
  message : @core.Message,
  mode~ : MimeMode = Strict,
) -> Unit {
  write_message_headers(writer, message, mode)
  writer.write_text("\r\n")
  write_body(writer, message.body, [], mode)
}

///|
fn min_int(left : Int32, right : Int32) -> Int32 {
  if left < right {
    left
  } else {
    right
  }
}

///|
pub fn infer_content_type(filename : String) -> @core.ContentType {
  let extension = match filename.rev_split_once(".") {
    Some((_, ext)) => ext.to_owned().to_lower()
    None => ""
  }
  match extension {
    "txt" => @core.ContentType::new("text", "plain", [])
    "md" => @core.ContentType::new("text", "markdown", [])
    "html" | "htm" => @core.ContentType::new("text", "html", [])
    "json" => @core.ContentType::new("application", "json", [])
    "csv" => @core.ContentType::new("text", "csv", [])
    "xml" => @core.ContentType::new("application", "xml", [])
    "pdf" => @core.ContentType::new("application", "pdf", [])
    "png" => @core.ContentType::new("image", "png", [])
    "jpg" | "jpeg" => @core.ContentType::new("image", "jpeg", [])
    "gif" => @core.ContentType::new("image", "gif", [])
    "webp" => @core.ContentType::new("image", "webp", [])
    "svg" => @core.ContentType::new("image", "svg+xml", [])
    "zip" => @core.ContentType::new("application", "zip", [])
    "gz" => @core.ContentType::new("application", "gzip", [])
    "tar" => @core.ContentType::new("application", "x-tar", [])
    "mbt" => @core.ContentType::new("text", "plain", [])
    "jsonl" => @core.ContentType::new("application", "x-ndjson", [])
    _ => @core.ContentType::application_octet_stream()
  }
}

///|
pub fn parse_rfc5322(
  input : String,
  mode~ : MimeMode = Lenient,
) -> @core.Message raise ParseError {
  let normalized = normalize_message_input(input, mode)
  let (raw_headers, raw_body) = split_headers_and_body(normalized)
  let headers = parse_headers_block(raw_headers, mode)

  let from = parse_single_mailbox(required_header(headers, "From"))
  let to = parse_mailbox_list(optional_header(headers, "To").unwrap_or(""))
  let cc = parse_mailbox_list(optional_header(headers, "Cc").unwrap_or(""))
  let bcc = parse_mailbox_list(optional_header(headers, "Bcc").unwrap_or(""))
  let subject = decode_encoded_words(
    optional_header(headers, "Subject").unwrap_or(""),
  )
  let custom_headers = collect_message_headers(headers)
  let body = parse_entity(headers, raw_body, mode)

  match @core.Message::new(from, to, cc, bcc, subject, body, custom_headers) {
    Ok(message) => message
    Err(error) => raise CoreValidation(error)
  }
}

///|
fn write_message_headers_sync(
  builder : StringBuilder,
  message : @core.Message,
  mode : MimeMode,
) -> Unit {
  builder.write_string(
    header_line("From", format_mailboxes([message.from]), mode),
  )
  builder.write_string(header_line("To", format_mailboxes(message.to), mode))
  if !message.cc.is_empty() {
    builder.write_string(header_line("Cc", format_mailboxes(message.cc), mode))
  }
  builder.write_string(
    header_line("Subject", encode_unstructured_header(message.subject), mode),
  )
  for header in message.headers {
    builder.write_string(
      header_line(header.name, encode_unstructured_header(header.value), mode),
    )
  }
  write_body_headers_sync(builder, message.body, [], mode)
}

///|
async fn[W : MimeWriter] write_message_headers(
  writer : W,
  message : @core.Message,
  mode : MimeMode,
) -> Unit {
  writer.write_text(header_line("From", format_mailboxes([message.from]), mode))
  writer.write_text(header_line("To", format_mailboxes(message.to), mode))
  if !message.cc.is_empty() {
    writer.write_text(header_line("Cc", format_mailboxes(message.cc), mode))
  }
  writer.write_text(
    header_line("Subject", encode_unstructured_header(message.subject), mode),
  )
  for header in message.headers {
    writer.write_text(
      header_line(header.name, encode_unstructured_header(header.value), mode),
    )
  }
  write_body_headers(writer, message.body, [], mode)
}

///|
fn write_body_headers_sync(
  builder : StringBuilder,
  body : @core.Body,
  path : Array[Int32],
  mode : MimeMode,
) -> Unit {
  for field in body_header_fields(body, path) {
    let (name, value) = field
    builder.write_string(header_line(name, value, mode))
  }
}

///|
async fn[W : MimeWriter] write_body_headers(
  writer : W,
  body : @core.Body,
  path : Array[Int32],
  mode : MimeMode,
) -> Unit {
  for field in body_header_fields(body, path) {
    let (name, value) = field
    writer.write_text(header_line(name, value, mode))
  }
}

///|
fn body_header_fields(
  body : @core.Body,
  path : Array[Int32],
) -> Array[(String, String)] {
  match body {
    Text(text) =>
      [
        (
          "Content-Type",
          format_text_content_type(text.content_type, text.charset),
        ),
        (
          "Content-Transfer-Encoding",
          format_transfer_encoding(text.transfer_encoding),
        ),
      ]
    Binary(binary) => {
      let fields : Array[(String, String)] = [
        ("Content-Type", format_content_type(binary.content_type)),
        ("Content-Disposition", format_content_disposition(binary.disposition)),
        (
          "Content-Transfer-Encoding",
          format_transfer_encoding(binary.transfer_encoding),
        ),
      ]
      match binary.content_id {
        Some(content_id) =>
          fields.push(("Content-Id", "<" + content_id.value + ">"))
        None => ()
      }
      fields
    }
    Multipart(kind, _) => {
      let boundary = boundary_for(path)
      [
        (
          "Content-Type",
          "multipart/" +
          format_multipart_kind(kind) +
          "; boundary=\"" +
          boundary +
          "\"",
        ),
      ]
    }
  }
}

///|
fn write_body_sync(
  builder : StringBuilder,
  body : @core.Body,
  path : Array[Int32],
  mode : MimeMode,
) -> Unit {
  ignore(mode)
  match body {
    Text(text) =>
      builder.write_string(
        encode_text_content(text.content, text.transfer_encoding),
      )
    Binary(binary) =>
      match binary.source {
        InMemory(data) =>
          builder.write_string(
            format_binary_payload_bytes(data, binary.transfer_encoding),
          )
        FilePath(_) =>
          abort(
            "file-backed attachments require async render_to_rfc5322/write_to",
          )
      }
    Multipart(kind, parts) => {
      let boundary = boundary_for(path)
      for index, part in parts.iter2() {
        builder.write_string("--" + boundary + "\r\n")
        let child_path = append_path(path, index)
        write_body_headers_sync(builder, part, child_path, mode)
        builder.write_string("\r\n")
        write_body_sync(builder, part, child_path, mode)
        builder.write_string("\r\n")
      }
      builder.write_string("--" + boundary + "--")
      ignore(kind)
    }
  }
}

///|
async fn[W : MimeWriter] write_body(
  writer : W,
  body : @core.Body,
  path : Array[Int32],
  mode : MimeMode,
) -> Unit {
  ignore(mode)
  match body {
    Text(text) =>
      writer.write_text(
        encode_text_content(text.content, text.transfer_encoding),
      )
    Binary(binary) =>
      write_binary_payload(writer, binary.source, binary.transfer_encoding)
    Multipart(kind, parts) => {
      let boundary = boundary_for(path)
      for index, part in parts.iter2() {
        writer.write_text("--" + boundary + "\r\n")
        let child_path = append_path(path, index)
        write_body_headers(writer, part, child_path, mode)
        writer.write_text("\r\n")
        write_body(writer, part, child_path, mode)
        writer.write_text("\r\n")
      }
      writer.write_text("--" + boundary + "--")
      ignore(kind)
    }
  }
}

///|
async fn[W : MimeWriter] write_binary_payload(
  writer : W,
  source : @core.AttachmentSource,
  encoding : @core.TransferEncoding,
) -> Unit {
  match (source, encoding) {
    (InMemory(data), Base64) =>
      write_base64_lines(writer, Bytes::from_array(data))
    (InMemory(data), _) => writer.write_text(bytes_to_ascii_string(data))
    (FilePath(path), Base64) => write_base64_file(writer, path)
    (FilePath(path), _) => write_raw_file(writer, path)
  }
}

///|
async fn[W : MimeWriter] write_base64_lines(writer : W, data : Bytes) -> Unit {
  let mut first = true
  for start = 0; start < data.length(); {
    let end = min_int(start + 57, data.length())
    if !first {
      writer.write_text("\r\n")
    }
    writer.write_text(@base64.encode(data[start:end]))
    first = false
    continue end
  }
}

///|
async fn[W : MimeWriter] write_base64_file(writer : W, path : String) -> Unit {
  let file = @fs.open(path, mode=ReadOnly)
  defer file.close()
  let pending : Array[Byte] = []
  let mut first = true
  while file.read_some(max_len=4096) is Some(chunk) {
    for byte in chunk {
      pending.push(byte)
    }
    while pending.length() >= 57 {
      if !first {
        writer.write_text("\r\n")
      }
      writer.write_text(@base64.encode(Bytes::from_array(pending[:57])))
      let remaining = pending[57:].to_owned()
      pending.clear()
      for byte in remaining {
        pending.push(byte)
      }
      first = false
    }
  }
  if !pending.is_empty() {
    if !first {
      writer.write_text("\r\n")
    }
    writer.write_text(@base64.encode(Bytes::from_array(pending[:])))
  }
}

///|
async fn[W : MimeWriter] write_raw_file(writer : W, path : String) -> Unit {
  let file = @fs.open(path, mode=ReadOnly)
  defer file.close()
  while file.read_some(max_len=4096) is Some(chunk) {
    writer.write_text(bytes_to_ascii_string(chunk.to_array()))
  }
}

///|
fn header_line(name : String, value : String, mode : MimeMode) -> String {
  let raw = name + ": " + value
  match mode {
    Strict => fold_header_line(raw, 78) + "\r\n"
    Lenient => raw + "\r\n"
  }
}

///|
fn fold_header_line(line : String, max_len : Int32) -> String {
  if line.length() <= max_len {
    return line
  }
  let tokens = line.split(" ").to_array()
  if tokens.is_empty() {
    return line
  }
  let builder = StringBuilder()
  let mut current = tokens[0].to_owned()
  for token_view in tokens[1:] {
    let token = token_view.to_owned()
    let candidate = current + " " + token
    if candidate.length() <= max_len {
      current = candidate
    } else {
      builder.write_string(current)
      builder.write_string("\r\n ")
      current = token
    }
  }
  builder.write_string(current)
  builder.to_string()
}

///|
fn format_mailboxes(mailboxes : Array[@core.Mailbox]) -> String {
  mailboxes.map(format_mailbox).join(", ")
}

///|
fn format_mailbox(mailbox : @core.Mailbox) -> String {
  match mailbox.display_name {
    Some(display_name) =>
      encode_phrase(display_name) + " <" + mailbox.address + ">"
    None => mailbox.address
  }
}

///|
fn format_text_content_type(
  content_type : @core.ContentType,
  charset : @core.Charset,
) -> String {
  format_content_type_with_extra(content_type, [
    ("charset", format_charset(charset)),
  ])
}

///|
fn format_content_type(content_type : @core.ContentType) -> String {
  format_content_type_with_extra(content_type, [])
}

///|
fn format_content_type_with_extra(
  content_type : @core.ContentType,
  extra_parameters : Array[(String, String)],
) -> String {
  let base = content_type.top_level + "/" + content_type.sub_type
  let parameters = [
    for param in content_type.parameters => (param.name, param.value)
  ]
  for parameter in extra_parameters {
    parameters.push(parameter)
  }
  if parameters.is_empty() {
    base
  } else {
    base + "; " + format_header_params(parameters)
  }
}

///|
fn format_content_disposition(disposition : @core.ContentDisposition) -> String {
  let parts : Array[String] = [format_disposition_kind(disposition.kind)]
  match disposition.filename {
    Some(filename) => parts.push(format_parameter("filename", filename))
    None => ()
  }
  if !disposition.parameters.is_empty() {
    parts.push(
      format_header_params(
        [
          for param in disposition.parameters => (param.name, param.value)
        ],
      ),
    )
  }
  parts.join("; ")
}

///|
fn format_header_params(parameters : Array[(String, String)]) -> String {
  parameters
  .map(parameter => format_parameter(parameter.0, parameter.1))
  .join("; ")
}

///|
fn format_parameter(name : String, value : String) -> String {
  if needs_rfc2231_encoding(value) {
    name + "*=UTF-8''" + percent_encode_utf8(value)
  } else if name != "filename" && is_parameter_token(value) {
    name + "=" + value
  } else {
    name + "=\"" + escape_quoted_string(value) + "\""
  }
}

///|
fn escape_quoted_string(value : String) -> String {
  value.replace_all(old="\\", new="\\\\").replace_all(old="\"", new="\\\"")
}

///|
fn needs_rfc2231_encoding(value : String) -> Bool {
  for c in value.iter() {
    if c == '"' || c == '\\' || c == ';' {
      return true
    }
    let code = c.to_int()
    if code < 32 || code > 126 {
      return true
    }
  }
  false
}

///|
fn is_parameter_token(value : String) -> Bool {
  if value == "" {
    return false
  }
  for c in value.iter() {
    let code = c.to_int()
    if !(code is ('0'..<='9' | 'A'..<='Z' | 'a'..<='z' | '-' | '_' | '.')) {
      return false
    }
  }
  true
}

///|
fn percent_encode_utf8(value : String) -> String {
  let bytes = @utf8.encode(value)
  let builder = StringBuilder()
  for byte in bytes {
    let code = byte.to_int()
    if is_attr_char(code) {
      builder.write_char(code.unsafe_to_char())
    } else {
      builder.write_char('%')
      builder.write_string(to_upper_hex(code))
    }
  }
  builder.to_string()
}

///|
fn is_attr_char(code : Int32) -> Bool {
  code is ('0'..<='9' | 'A'..<='Z' | 'a'..<='z') ||
  code
  is ('!' | '#' | '$' | '&' | '+' | '-' | '.' | '^' | '_' | '`' | '|' | '~')
}

///|
fn encode_phrase(value : String) -> String {
  if needs_encoded_word(value) {
    encode_encoded_words(value)
  } else {
    value
  }
}

///|
fn encode_unstructured_header(value : String) -> String {
  if needs_encoded_word(value) {
    encode_encoded_words(value)
  } else {
    value
  }
}

///|
fn needs_encoded_word(value : String) -> Bool {
  for c in value.iter() {
    let code = c.to_int()
    if code < 32 || code > 126 {
      return true
    }
  }
  false
}

///|
fn encode_encoded_words(value : String) -> String {
  let bytes = @utf8.encode(value)
  let words : Array[String] = []
  for start = 0; start < bytes.length(); {
    let end = min_int(start + 30, bytes.length())
    words.push("=?UTF-8?B?" + @base64.encode(bytes[start:end]) + "?=")
    continue end
  }
  words.join(" ")
}

///|
fn format_charset(charset : @core.Charset) -> String {
  match charset {
    Utf8 => "utf-8"
    UsAscii => "us-ascii"
    Other(name) => name
  }
}

///|
fn format_transfer_encoding(encoding : @core.TransferEncoding) -> String {
  match encoding {
    SevenBit => "7bit"
    EightBit => "8bit"
    QuotedPrintable => "quoted-printable"
    Base64 => "base64"
    Binary => "binary"
  }
}

///|
fn format_disposition_kind(kind : @core.DispositionKind) -> String {
  match kind {
    Inline => "inline"
    Attachment => "attachment"
    Other(name) => name
  }
}

///|
fn format_multipart_kind(kind : @core.MultipartKind) -> String {
  match kind {
    Mixed => "mixed"
    Alternative => "alternative"
    Related => "related"
    Custom(name) => name
  }
}

///|
fn encode_text_content(
  content : String,
  encoding : @core.TransferEncoding,
) -> String {
  match encoding {
    QuotedPrintable => encode_quoted_printable(content)
    Base64 =>
      format_binary_payload_bytes(
        @utf8.encode(content).to_array(),
        @core.TransferEncoding::base64(),
      )
    _ => normalize_text_newlines(content)
  }
}

///|
fn encode_quoted_printable(content : String) -> String {
  let normalized = normalize_text_newlines(content)
  let bytes = @utf8.encode(normalized)
  let builder = StringBuilder()
  let mut line_len = 0
  for index in 0.. 75 {
      builder.write_string("=\r\n")
      line_len = 0
    }
    builder.write_string(chunk)
    line_len += chunk.length()
  }
  builder.to_string()
}

///|
fn quoted_printable_chunk(bytes : Bytes, index : Int32) -> String {
  let byte = bytes[index]
  let value = byte.to_int()
  let is_trailing_whitespace = (byte == b' ' || byte == b'\t') &&
    (
      index + 1 == bytes.length() ||
      (
        bytes[index + 1] == b'\r' &&
        index + 2 < bytes.length() &&
        bytes[index + 2] == b'\n'
      )
    )
  if should_escape_quoted_printable_byte(byte) || is_trailing_whitespace {
    "=" + to_upper_hex(value)
  } else {
    String::from_array([value.unsafe_to_char()])
  }
}

///|
fn normalize_text_newlines(content : String) -> String {
  let without_crlf = content.replace_all(old="\r\n", new="\n")
  without_crlf.replace_all(old="\r", new="\n").replace_all(old="\n", new="\r\n")
}

///|
fn should_escape_quoted_printable_byte(byte : Byte) -> Bool {
  let value = byte.to_int()
  byte == b'=' || value < 32 || value > 126 || byte == b'\x7f'
}

///|
fn format_binary_payload_bytes(
  data : Array[Byte],
  encoding : @core.TransferEncoding,
) -> String {
  match encoding {
    Base64 => encode_base64(data)
    _ => bytes_to_ascii_string(data)
  }
}

///|
fn encode_base64(data : Array[Byte]) -> String {
  encode_base64_mime(Bytes::from_array(data))
}

///|
fn encode_base64_mime(data : Bytes) -> String {
  let builder = StringBuilder()
  let mut first = true
  for start = 0; start < data.length(); {
    let end = min_int(start + 57, data.length())
    if !first {
      builder.write_string("\r\n")
    }
    builder.write_string(@base64.encode(data[start:end]))
    first = false
    continue end
  }
  builder.to_string()
}

///|
fn bytes_to_ascii_string(data : Array[Byte]) -> String {
  let builder = StringBuilder()
  for byte in data {
    builder.write_char(byte.to_int().unsafe_to_char())
  }
  builder.to_string()
}

///|
fn to_upper_hex(value : Int32) -> String {
  let digits = "0123456789ABCDEF"
  let high = (value >> 4) & 15
  let low = value & 15
  String::from_array([
    digits[high].unsafe_to_char(),
    digits[low].unsafe_to_char(),
  ])
}

///|
fn boundary_for(path : Array[Int32]) -> String {
  if path.is_empty() {
    "moonbit-email-boundary-root"
  } else {
    "moonbit-email-boundary-" + path.map(index => index.to_string()).join("-")
  }
}

///|
fn append_path(path : Array[Int32], index : Int32) -> Array[Int32] {
  let next = path.copy()
  next.push(index)
  next
}

///|
fn contains_file_source(body : @core.Body) -> Bool {
  match body {
    Text(_) => false
    Binary(binary) =>
      match binary.source {
        InMemory(_) => false
        FilePath(_) => true
      }
    Multipart(_, parts) => parts.any(contains_file_source)
  }
}

///|
fn normalize_message_input(
  input : String,
  mode : MimeMode,
) -> String raise ParseError {
  match mode {
    Strict => {
      if input.contains("\r") && !input.contains("\r\n") {
        raise Malformed("strict mode requires CRLF newlines")
      }
      if input.contains("\n") && !input.contains("\r\n") {
        raise Malformed("strict mode requires CRLF newlines")
      }
      input
    }
    Lenient => normalize_text_newlines(input)
  }
}

///|
fn split_headers_and_body(input : String) -> (String, String) raise ParseError {
  match input.split_once("\r\n\r\n") {
    Some((headers, body)) => (headers.to_owned(), body.to_owned())
    None => raise Malformed("missing header/body separator")
  }
}

///|
fn parse_headers_block(
  block : String,
  mode : MimeMode,
) -> Array[(String, String)] raise ParseError {
  ignore(mode)
  let lines = block.split("\r\n").to_array()
  let headers : Array[(String, String)] = []
  let mut current_name = ""
  let mut current_value = ""
  for line_view in lines {
    let line = line_view.to_owned()
    if line.has_prefix(" ") || line.has_prefix("\t") {
      if current_name == "" {
        raise Malformed("folded header without a leading field")
      }
      current_value = current_value + " " + line.trim().to_owned()
      continue
    }
    if current_name != "" {
      headers.push((current_name, current_value))
    }
    match line.split_once(":") {
      Some((name, value)) => {
        current_name = name.to_owned()
        current_value = value.trim().to_owned()
      }
      None => raise Malformed("invalid header line: " + line)
    }
  }
  if current_name != "" {
    headers.push((current_name, current_value))
  }
  headers
}

///|
fn header_name_matches(actual : String, expected : String) -> Bool {
  actual.to_lower() == expected.to_lower()
}

///|
fn required_header(
  headers : Array[(String, String)],
  name : String,
) -> String raise ParseError {
  match optional_header(headers, name) {
    Some(value) => value
    None => raise Malformed("missing required header: " + name)
  }
}

///|
fn optional_header(headers : Array[(String, String)], name : String) -> String? {
  for header_entry in headers {
    let (header_name, value) = header_entry
    if header_name_matches(header_name, name) {
      return Some(value)
    }
  }
  None
}

///|
fn collect_message_headers(
  headers : Array[(String, String)],
) -> Array[@core.Header] raise ParseError {
  let collected : Array[@core.Header] = []
  for header_entry in headers {
    let (name, value) = header_entry
    if is_message_header(name) {
      ()
    } else if is_body_header(name) {
      ()
    } else {
      match @core.Header::new(name, decode_encoded_words(value)) {
        Ok(header) => collected.push(header)
        Err(error) => raise CoreValidation(error)
      }
    }
  }
  collected
}

///|
fn is_message_header(name : String) -> Bool {
  header_name_matches(name, "From") ||
  header_name_matches(name, "To") ||
  header_name_matches(name, "Cc") ||
  header_name_matches(name, "Bcc") ||
  header_name_matches(name, "Subject")
}

///|
fn is_body_header(name : String) -> Bool {
  header_name_matches(name, "Content-Type") ||
  header_name_matches(name, "Content-Transfer-Encoding") ||
  header_name_matches(name, "Content-Disposition") ||
  header_name_matches(name, "Content-Id")
}

///|
fn parse_single_mailbox(value : String) -> @core.Mailbox raise ParseError {
  match parse_mailbox_list(value) {
    [mailbox] => mailbox
    [] => raise Malformed("expected a mailbox")
    _ => raise Malformed("expected a single mailbox")
  }
}

///|
fn parse_mailbox_list(value : String) -> Array[@core.Mailbox] raise ParseError {
  let trimmed = value.trim()
  if trimmed == "" {
    return []
  }
  let mailboxes : Array[@core.Mailbox] = []
  for segment in split_mailbox_segments(trimmed.to_owned()) {
    mailboxes.push(parse_mailbox(segment))
  }
  mailboxes
}

///|
fn split_mailbox_segments(value : String) -> Array[String] {
  let segments : Array[String] = []
  let mut current = ""
  let mut angle_depth = 0
  let mut in_quotes = false
  for ch in value.iter() {
    if ch == '"' {
      in_quotes = !in_quotes
      current = current + ch.to_string()
    } else if ch == '<' && !in_quotes {
      angle_depth += 1
      current = current + ch.to_string()
    } else if ch == '>' && !in_quotes && angle_depth > 0 {
      angle_depth -= 1
      current = current + ch.to_string()
    } else if ch == ',' && !in_quotes && angle_depth == 0 {
      segments.push(current.trim().to_owned())
      current = ""
    } else {
      current = current + ch.to_string()
    }
  }
  let trailing = current.trim().to_owned()
  if trailing != "" {
    segments.push(trailing)
  }
  segments
}

///|
fn parse_mailbox(value : String) -> @core.Mailbox raise ParseError {
  let trimmed = value.trim()
  match trimmed.split_once("<") {
    Some((display_name, address_tail)) => {
      let address_text = address_tail
        .before(">")
        .unwrap_or(address_tail)
        .trim()
        .to_owned()
      let display_text = display_name.trim().trim(chars="\"").to_owned()
      let normalized_display = if display_text == "" {
        None
      } else {
        Some(decode_encoded_words(display_text))
      }
      match @core.Mailbox::new(address_text, normalized_display) {
        Ok(mailbox) => mailbox
        Err(error) => raise CoreValidation(error)
      }
    }
    None =>
      match @core.Mailbox::address_only(trimmed.to_owned()) {
        Ok(mailbox) => mailbox
        Err(error) => raise CoreValidation(error)
      }
  }
}

///|
fn parse_entity(
  headers : Array[(String, String)],
  body_text : String,
  mode : MimeMode,
) -> @core.Body raise ParseError {
  let content_type_text = optional_header(headers, "Content-Type").unwrap_or(
    "text/plain; charset=utf-8",
  )
  let transfer_encoding_text = optional_header(
    headers, "Content-Transfer-Encoding",
  ).unwrap_or("7bit")
  let disposition_text = optional_header(headers, "Content-Disposition")
  let content_id_text = optional_header(headers, "Content-Id")

  let content_type = parse_content_type(content_type_text)
  let transfer_encoding = parse_transfer_encoding(transfer_encoding_text)
  if content_type.top_level.to_lower() == "multipart" {
    let boundary = find_parameter(content_type.parameters, "boundary")
    let boundary = match boundary {
      Some(value) => value
      None => raise Malformed("multipart entity missing boundary")
    }
    let parts = split_multipart_parts(body_text, boundary)
    let bodies : Array[@core.Body] = []
    for part_text in parts {
      let (part_headers_text, part_body_text) = split_headers_and_body(
        part_text,
      )
      let part_headers = parse_headers_block(part_headers_text, mode)
      bodies.push(parse_entity(part_headers, part_body_text, mode))
    }
    match
      @core.Body::multipart(multipart_kind_for(content_type.sub_type), bodies) {
      Ok(body) => body
      Err(error) => raise CoreValidation(error)
    }
  } else if content_type.top_level.to_lower() == "text" {
    let decoded_text = decode_text_body(body_text, transfer_encoding)
    let charset = charset_from_content_type(content_type)
    @core.Body::text(
      remove_parameter(content_type, "charset"),
      charset,
      transfer_encoding,
      decoded_text,
    )
  } else {
    let decoded_bytes = decode_binary_body(body_text, transfer_encoding)
    let disposition = parse_content_disposition(disposition_text)
    let filename = disposition.filename
    let content_id = parse_content_id(content_id_text)
    match
      @core.Body::binary(
        content_type,
        disposition,
        transfer_encoding,
        filename,
        content_id,
        @core.AttachmentSource::in_memory(decoded_bytes.to_array()),
      ) {
      Ok(body) => body
      Err(error) => raise CoreValidation(error)
    }
  }
}

///|
fn multipart_kind_for(sub_type : String) -> @core.MultipartKind {
  match sub_type.to_lower() {
    "mixed" => @core.MultipartKind::mixed()
    "alternative" => @core.MultipartKind::alternative()
    "related" => @core.MultipartKind::related()
    other => @core.MultipartKind::custom(other)
  }
}

///|
fn charset_from_content_type(content_type : @core.ContentType) -> @core.Charset {
  match find_parameter(content_type.parameters, "charset") {
    Some(value) =>
      match value.to_lower() {
        "utf-8" => @core.Charset::utf8()
        "us-ascii" => @core.Charset::us_ascii()
        other => @core.Charset::other(other)
      }
    None => @core.Charset::utf8()
  }
}

///|
fn parse_content_type(value : String) -> @core.ContentType raise ParseError {
  let segments = split_parameter_segments(value)
  if segments.is_empty() {
    raise Malformed("empty Content-Type")
  }
  let media_type = segments[0].trim().to_owned()
  match media_type.split_once("/") {
    Some((top_level, sub_type)) => {
      let parameters : Array[@core.HeaderParam] = []
      for segment in segments[1:] {
        match parse_parameter(segment) {
          Some((name, parameter_value)) =>
            parameters.push(@core.HeaderParam::new(name, parameter_value))
          None => ()
        }
      }
      @core.ContentType::new(
        top_level.trim().to_owned(),
        sub_type.trim().to_owned(),
        parameters,
      )
    }
    None => raise Malformed("invalid Content-Type: " + value)
  }
}

///|
fn parse_content_disposition(
  value : String?,
) -> @core.ContentDisposition raise ParseError {
  match value {
    Some(text) => {
      let segments = split_parameter_segments(text)
      if segments.is_empty() {
        raise Malformed("empty Content-Disposition")
      }
      let kind = match segments[0].trim().to_lower() {
        "inline" => @core.DispositionKind::inline()
        "attachment" => @core.DispositionKind::attachment()
        other => @core.DispositionKind::other(other.to_owned())
      }
      let parameters : Array[@core.HeaderParam] = []
      let mut filename : String? = None
      for segment in segments[1:] {
        match parse_parameter(segment) {
          Some((name, parameter_value)) =>
            if name.to_lower() == "filename" {
              filename = Some(parameter_value)
            } else {
              parameters.push(@core.HeaderParam::new(name, parameter_value))
            }
          None => ()
        }
      }
      match filename {
        Some(name) =>
          match
            @core.ContentDisposition::new(kind, filename=name, parameters~) {
            Ok(disposition) => disposition
            Err(error) => raise CoreValidation(error)
          }
        None =>
          match @core.ContentDisposition::new(kind, parameters~) {
            Ok(disposition) => disposition
            Err(error) => raise CoreValidation(error)
          }
      }
    }
    None =>
      match @core.ContentDisposition::new(@core.DispositionKind::attachment()) {
        Ok(disposition) => disposition
        Err(error) => raise CoreValidation(error)
      }
  }
}

///|
fn parse_parameter(segment : StringView) -> (String, String)? raise ParseError {
  let trimmed = segment.trim()
  if trimmed == "" {
    return None
  }
  match trimmed.split_once("=") {
    Some((raw_name, raw_value)) => {
      let name = raw_name.trim().to_owned()
      let value = raw_value.trim().to_owned()
      if name.has_suffix("*") {
        Some(
          (name[0:name.length() - 1].to_owned(), decode_rfc2231_value(value)),
        )
      } else {
        Some((name, strip_parameter_value(value)))
      }
    }
    None => raise Malformed("invalid parameter: " + trimmed.to_owned())
  }
}

///|
fn split_parameter_segments(value : String) -> Array[String] {
  let segments : Array[String] = []
  let mut current = ""
  let mut in_quotes = false
  for ch in value.iter() {
    if ch == '"' {
      in_quotes = !in_quotes
      current = current + ch.to_string()
    } else if ch == ';' && !in_quotes {
      segments.push(current.trim().to_owned())
      current = ""
    } else {
      current = current + ch.to_string()
    }
  }
  segments.push(current.trim().to_owned())
  segments
}

///|
fn decode_rfc2231_value(value : String) -> String raise ParseError {
  let stripped = strip_parameter_value(value)
  let encoded = match stripped.split_once("''") {
    Some((_, rest)) => rest.to_owned()
    None => stripped
  }
  let bytes = decode_percent_bytes(encoded)
  @utf8.decode(bytes) catch {
    _ => raise Malformed("invalid RFC2231 UTF-8 parameter")
  }
}

///|
fn strip_parameter_value(value : String) -> String {
  value.trim().trim(chars="\"").to_owned()
}

///|
fn find_parameter(
  parameters : Array[@core.HeaderParam],
  name : String,
) -> String? {
  for parameter in parameters {
    if parameter.name.to_lower() == name.to_lower() {
      return Some(parameter.value)
    }
  }
  None
}

///|
fn remove_parameter(
  content_type : @core.ContentType,
  name : String,
) -> @core.ContentType {
  @core.ContentType::new(
    content_type.top_level,
    content_type.sub_type,
    content_type.parameters.filter(param => {
      param.name.to_lower() != name.to_lower()
    }),
  )
}

///|
fn parse_transfer_encoding(
  value : String,
) -> @core.TransferEncoding raise ParseError {
  match value.trim().to_lower() {
    "7bit" => @core.TransferEncoding::seven_bit()
    "8bit" => @core.TransferEncoding::eight_bit()
    "quoted-printable" => @core.TransferEncoding::quoted_printable()
    "base64" => @core.TransferEncoding::base64()
    "binary" => @core.TransferEncoding::binary()
    other =>
      raise Unsupported("unsupported transfer encoding: " + other.to_owned())
  }
}

///|
fn parse_content_id(value : String?) -> @core.ContentId? {
  match value {
    Some(text) =>
      match @core.ContentId::new(text.trim().trim(chars="<>").to_owned()) {
        Ok(content_id) => Some(content_id)
        Err(_) => None
      }
    None => None
  }
}

///|
fn split_multipart_parts(
  body_text : String,
  boundary : String,
) -> Array[String] raise ParseError {
  let lines = body_text.split("\r\n").to_array()
  let start_marker = "--" + boundary
  let end_marker = start_marker + "--"
  let parts : Array[String] = []
  let mut current = ""
  let mut in_part = false
  let mut saw_boundary = false
  for line_view in lines {
    let line = line_view.to_owned()
    if line == start_marker {
      saw_boundary = true
      if in_part {
        parts.push(trim_trailing_crlf(current))
        current = ""
      }
      in_part = true
      continue
    }
    if line == end_marker {
      if in_part {
        parts.push(trim_trailing_crlf(current))
      }
      return parts
    }
    if in_part {
      current = current + line + "\r\n"
    }
  }
  if !saw_boundary {
    raise Malformed("multipart boundary not found")
  }
  parts
}

///|
fn trim_trailing_crlf(value : String) -> String {
  if value.has_suffix("\r\n") {
    value[0:value.length() - 2].to_owned()
  } else {
    value
  }
}

///|
fn decode_text_body(
  body_text : String,
  transfer_encoding : @core.TransferEncoding,
) -> String raise ParseError {
  match transfer_encoding {
    QuotedPrintable => bytes_to_text(decode_quoted_printable_bytes(body_text))
    Base64 => decode_base64_text(body_text)
    _ => body_text.replace_all(old="\r\n", new="\n")
  }
}

///|
fn decode_binary_body(
  body_text : String,
  transfer_encoding : @core.TransferEncoding,
) -> Bytes raise ParseError {
  match transfer_encoding {
    QuotedPrintable => decode_quoted_printable_bytes(body_text)
    Base64 => decode_base64_bytes(body_text)
    _ => @utf8.encode(body_text)
  }
}

///|
fn decode_base64_text(body_text : String) -> String raise ParseError {
  bytes_to_text(decode_base64_bytes(body_text))
}

///|
fn decode_base64_bytes(body_text : String) -> Bytes raise ParseError {
  @base64.decode(body_text, ignore_whitespace=true) catch {
    _ => raise Malformed("invalid base64 payload")
  }
}

///|
fn bytes_to_text(bytes : Bytes) -> String raise ParseError {
  @utf8.decode(bytes).replace_all(old="\r\n", new="\n") catch {
    _ => raise Malformed("invalid UTF-8 text payload")
  }
}

///|
fn decode_quoted_printable_bytes(body_text : String) -> Bytes raise ParseError {
  let bytes = @utf8.encode(body_text)
  let output : Array[Byte] = []
  for index = 0; index < bytes.length(); {
    let byte = bytes[index]
    if byte == b'=' {
      if index + 2 < bytes.length() &&
        bytes[index + 1] == b'\r' &&
        bytes[index + 2] == b'\n' {
        continue index + 3
      }
      guard index + 2 < bytes.length() else {
        raise Malformed("truncated quoted-printable escape")
      }
      let high = hex_digit(bytes[index + 1].to_int().unsafe_to_char())
      let low = hex_digit(bytes[index + 2].to_int().unsafe_to_char())
      match (high, low) {
        (Some(high), Some(low)) => {
          output.push((high * 16 + low).to_byte())
          continue index + 3
        }
        _ => raise Malformed("invalid quoted-printable escape")
      }
    } else if byte == b'\r' &&
      index + 1 < bytes.length() &&
      bytes[index + 1] == b'\n' {
      output.push(b'\n')
      continue index + 2
    } else {
      output.push(byte)
      continue index + 1
    }
  }
  Bytes::from_array(output)
}

///|
fn hex_digit(ch : Char) -> Int32? {
  match ch {
    '0'..<='9' => Some(ch.to_int() - '0'.to_int())
    'A'..<='F' => Some(ch.to_int() - 'A'.to_int() + 10)
    'a'..<='f' => Some(ch.to_int() - 'a'.to_int() + 10)
    _ => None
  }
}

///|
fn decode_percent_bytes(value : String) -> Bytes raise ParseError {
  let output : Array[Byte] = []
  for index = 0; index < value.length(); {
    let ch = value.code_unit_at(index).to_int().unsafe_to_char()
    if ch == '%' {
      guard index + 2 < value.length() else {
        raise Malformed("truncated percent escape")
      }
      let high = hex_digit(
        value.code_unit_at(index + 1).to_int().unsafe_to_char(),
      )
      let low = hex_digit(
        value.code_unit_at(index + 2).to_int().unsafe_to_char(),
      )
      match (high, low) {
        (Some(high), Some(low)) => {
          output.push((high * 16 + low).to_byte())
          continue index + 3
        }
        _ => raise Malformed("invalid percent escape")
      }
    } else {
      output.push(ch.to_int().to_byte())
      continue index + 1
    }
  }
  Bytes::from_array(output)
}

///|
fn decode_encoded_words(value : String) -> String raise ParseError {
  let builder = StringBuilder()
  let mut index = 0
  while index < value.length() {
    if index + 1 < value.length() &&
      value[index:index + 2] == "=?" &&
      find_encoded_word_end(value, index) is Some(end) {
      let token = value[index:end].to_owned()
      builder.write_string(decode_encoded_word(token))
      index = end
      while index < value.length() &&
            value.code_unit_at(index).to_int().unsafe_to_char() == ' ' {
        if index + 2 < value.length() && value[index + 1:index + 3] == "=?" {
          index += 1
        } else {
          builder.write_char(' ')
          index += 1
          break
        }
      }
    } else {
      builder.write_char(value.code_unit_at(index).to_int().unsafe_to_char())
      index += 1
    }
  }
  builder.to_string()
}

///|
fn find_encoded_word_end(value : String, start : Int32) -> Int32? {
  let marker = value[start:].find("?=")
  match marker {
    Some(offset) => Some(start + offset + 2)
    None => None
  }
}

///|
fn decode_encoded_word(token : String) -> String raise ParseError {
  guard token.has_prefix("=?") && token.has_suffix("?=") else {
    raise Malformed("invalid encoded-word")
  }
  let inner = token[2:token.length() - 2].to_owned()
  let parts = inner.split("?").to_array()
  guard parts.length() == 3 else {
    raise Malformed("invalid encoded-word parts")
  }
  let charset = parts[0].to_owned().to_lower()
  let encoding = parts[1].to_owned().to_upper()
  let payload = parts[2].to_owned()
  guard charset == "utf-8" else {
    raise Unsupported("only UTF-8 encoded-word headers are supported")
  }
  let bytes = match encoding {
    "B" => decode_base64_bytes(payload)
    "Q" => decode_encoded_word_q(payload)
    _ => raise Unsupported("unsupported encoded-word encoding: " + encoding)
  }
  bytes_to_text(bytes)
}

///|
fn decode_encoded_word_q(payload : String) -> Bytes raise ParseError {
  let normalized = payload.replace_all(old="_", new=" ")
  decode_quoted_printable_bytes(normalized)
}