///|
pub(all) struct Header {
  name : String
  value : String
} derive(Eq, Debug)

///|
pub struct Message {
  headers : Array[Header]
  size : Int
  envelope_from : String
  envelope_to : Array[String]
}

///|
pub(all) struct Mailbox {
  address : String
  localpart : String
  domain : String
} derive(Eq, Debug)

///|
fn valid_header_name(name : String) -> Bool {
  !name.is_empty() && name.all(c => c.is_ascii_graphic() && c != ':')
}

///|
fn single_line(value : String) -> Bool {
  !value.contains_char('\r') &&
  !value.contains_char('\n') &&
  !value.contains_char('\u{0}')
}

///|
pub fn Message::new(
  headers : Array[Header],
  size : Int,
  envelope_from? : String = "",
  envelope_to? : Array[String] = [],
  limits? : Limits = Limits::default(),
) -> Message raise SieveError {
  limits.check()
  if size < 0 {
    fail("message.size", "message size must be nonnegative", origin())
  }
  if headers.length() > limits.headers {
    fail("limit.headers", "too many headers", origin())
  }
  let mut total = 0
  for header in headers {
    if header.name.length() > limits.string_chars ||
      header.value.length() > limits.string_chars {
      fail("limit.string", "header name or value too long", origin())
    }
    if header.name.length() > limits.source_chars - total {
      fail("limit.message", "normalized message exceeds limit", origin())
    }
    total += header.name.length()
    if header.value.length() > limits.source_chars - total {
      fail("limit.message", "normalized message exceeds limit", origin())
    }
    total += header.value.length()
    if !valid_header_name(header.name) {
      fail("message.header_name", "invalid header field name", origin())
    }
    if !single_line(header.value) {
      fail(
        "message.header_value",
        "provide unfolded, NUL-free header values",
        origin(),
      )
    }
  }
  if envelope_to.length() > 1 {
    fail(
      "message.recipient",
      "Sieve runs for one recipient; split recipient simulations",
      origin(),
    )
  }
  if envelope_to.any(address => address.is_empty()) {
    fail(
      "message.recipient",
      "recipient cannot be an empty reverse path",
      origin(),
    )
  }
  for address in [envelope_from, ..envelope_to] {
    if address.length() > limits.string_chars {
      fail("limit.string", "envelope address too long", origin())
    }
    if address.length() > limits.source_chars - total {
      fail(
        "limit.message",
        "normalized message and envelope exceed limit",
        origin(),
      )
    }
    total += address.length()
    if !address.is_empty() && parse_mailbox(address) is None {
      fail(
        "message.envelope",
        "envelope must contain bare supported addr-spec values",
        origin(),
      )
    }
  }
  {
    headers: headers.copy(),
    size,
    envelope_from,
    envelope_to: envelope_to.copy(),
  }
}

///|
pub fn Message::header_values(self : Message, name : String) -> Array[String] {
  let key = ascii_lower(name)
  self.headers.filter_map(h => {
    if ascii_lower(h.name) == key {
      Some(h.value)
    } else {
      None
    }
  })
}

///|
pub fn Message::octet_size(self : Message) -> Int {
  self.size
}

///|
pub fn parse_mailbox(value : String) -> Mailbox? {
  let source = value.trim().to_owned()
  if source.is_empty() || source.length() > 65536 || !single_line(source) {
    return None
  }
  let chars = source.to_array()
  let mut quoted = false
  let mut escaped = false
  let mut at = -1
  for i = 0; i < chars.length(); i = i + 1 {
    let c = chars[i]
    if escaped {
      escaped = false
      continue
    }
    if quoted && c == '\\' {
      escaped = true
      continue
    }
    if c == '"' {
      quoted = !quoted
      continue
    }
    if !quoted && c == '@' {
      if at >= 0 {
        return None
      }
      at = i
    }
  }
  if quoted || escaped || at <= 0 || at == chars.length() - 1 {
    return None
  }
  let local_raw = chars_text(chars, 0, at)
  let domain = ascii_lower(chars_text(chars, at + 1, chars.length()))
  let localpart = if chars[0] == '"' {
    if at < 2 || chars[at - 1] != '"' {
      return None
    }
    let out = StringBuilder()
    let mut i = 1
    while i < at - 1 {
      if chars[i] == '\\' {
        i += 1
        if i >= at - 1 {
          return None
        }
      } else if chars[i] == '"' {
        return None
      }
      out.write_char(chars[i])
      i += 1
    }
    out.to_string()
  } else {
    if local_raw.has_prefix(".") ||
      local_raw.has_suffix(".") ||
      local_raw.contains("..") {
      return None
    }
    if !local_raw.all(c => {
        (c.is_ascii_alphabetic() || c.is_ascii_digit()) ||
        "!#$%&'*+-/=?^_\u{60}{|}~.".contains_char(c)
      }) {
      return None
    }
    local_raw
  }
  if domain.has_prefix("[") && domain.has_suffix("]") {
    if domain.length() < 3 || !domain.all(c => c.is_ascii_graphic() && c != '@') {
      return None
    }
  } else {
    if domain.is_empty() ||
      domain.has_prefix(".") ||
      domain.has_suffix(".") ||
      domain.contains("..") {
      return None
    }
    for label in domain.split(".") {
      let s = label.to_owned()
      if s.is_empty() ||
        s.has_prefix("-") ||
        s.has_suffix("-") ||
        !s.all(c => (c.is_ascii_alphabetic() || c.is_ascii_digit()) || c == '-') {
        return None
      }
    }
  }
  Some({ address: source, localpart, domain })
}

///|
fn mailbox_piece(piece : String) -> Mailbox? {
  let value = piece.trim().to_owned()
  match value.split_once("<") {
    Some((_, rest)) =>
      match rest.to_owned().split_once(">") {
        Some((address, tail)) if tail.to_owned().trim().is_empty() =>
          parse_mailbox(address.to_owned())
        _ => None
      }
    None => parse_mailbox(value)
  }
}

///|
pub fn extract_mailboxes(
  value : String,
  limits? : Limits = Limits::default(),
) -> Array[Mailbox] raise SieveError {
  limits.check()
  if value.length() > limits.string_chars {
    fail("limit.string", "address header too long", origin())
  }
  let out : Array[Mailbox] = []
  let piece = StringBuilder()
  let mut quoted = false
  let mut escaped = false
  let mut angle = false
  let mut comment = 0
  for c in value {
    if escaped {
      if comment == 0 {
        piece.write_char(c)
      }
      escaped = false
      continue
    }
    if (quoted || comment > 0) && c == '\\' {
      if comment == 0 {
        piece.write_char(c)
      }
      escaped = true
      continue
    }
    if comment > 0 {
      if c == '(' {
        comment += 1
      }
      if c == ')' {
        comment -= 1
      }
      if comment > limits.depth {
        fail("limit.depth", "address comment nesting too deep", origin())
      }
      continue
    }
    if c == '"' {
      quoted = !quoted
      piece.write_char(c)
      continue
    }
    if !quoted {
      if c == '(' {
        comment = 1
        continue
      }
      if c == '<' {
        angle = true
      }
      if c == '>' {
        angle = false
      }
      if !angle && c == ':' {
        piece.reset()
        continue
      }
      if !angle && (c == ',' || c == ';') {
        match mailbox_piece(piece.to_string()) {
          Some(m) => out.push(m)
          None => ()
        }
        piece.reset()
        if out.length() > limits.headers {
          fail("limit.addresses", "too many addresses", origin())
        }
        continue
      }
    }
    piece.write_char(c)
  }
  if !quoted && comment == 0 && !angle && !escaped {
    match mailbox_piece(piece.to_string()) {
      Some(m) => out.push(m)
      None => ()
    }
  }
  if out.length() > limits.headers {
    fail("limit.addresses", "too many addresses", origin())
  }
  out
}